From 0e4b986e81f8132a44e61fa8df18860301cc3468 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 20:31:10 +0000
Subject: [PATCH] =JakeVan preliminary additions
---
build/list/render.php | 2
base/_setup.php | 11
assets/css/icons.css | 1
src/drawer-menu/index.php | 0
build/drawer-menu/style-index-rtl.css | 1
src/summary/render.php | 2
assets/js/concise/navigation.js | 2
build/drawer-menu/index-rtl.css | 1
assets/js/min/navigation.min.js | 2
inc/blocks/SummaryBlock.php | 4
build/drawer-menu/index.js | 1
assets/css/dash.min.css | 2
inc/rest/routes/NotificationsRoutes.php | 1
build/drawer-menu/index.css | 1
inc/helpers/ui.php | 14
inc/forms/TaxonomySelector.php | 1
src/feed/render.php | 3
wp-config.php | 128
inc/rest/routes/ContentRoutes.php | 3
assets/js/concise/UtilityFunctions.js | 20
assets/js/min/queue.min.js | 2
inc/blocks/CustomBlocks.php | 70
inc/rest/routes/FeedRoutes.php | 41
assets/js/min/utility.min.js | 2
assets/js/concise/CRUD.js | 21
JVBase.php | 34
build/summary/render.php | 2
assets/js/concise/UploadManagerC.js | 2278 ++++++++
inc/blocks/TimelineBlock.php | 4
src/drawer-menu/index.js | 10
assets/js/concise/Queue.js | 25
inc/ui/CRUDSkeleton.php | 3
inc/integrations/Integrations.php | 23
src/feed/style.scss | 84
inc/managers/IconsManager.php | 1
build/drawer-menu/render.php | 39
src/drawer-menu/style.scss | 88
assets/js/min/uploadGroups.min.js | 1
build/feed/style-index-rtl.css | 2
inc/helpers/breadcrumbs.php | 5
inc/ui/Navigation.php | 170
src/drawer-menu/save.js | 3
src/forms/view.js | 63
assets/js/concise/HandleSelection.js | 551 -
assets/js/concise/FormController.js | 4
build/feed/style-index.css | 2
assets/js/concise/UploadManagerOld.js | 3141 ++++++++++++
inc/managers/DirectoryManager.php | 335
assets/js/min/handleSelection.min.js | 2
inc/registry/CheckCustomTables.php | 237
inc/rest/routes/FormRoutes.php | 2
assets/js/concise/UploadManager.js | 4628 ++++++-----------
inc/blocks/FeedBlock.php | 194
inc/managers/SEO/BreadcrumbManager.php | 63
src/feed/view.js | 135
inc/utility/Validator.php | 2
assets/js/min/crud.min.js | 2
package-lock.json | 2288 ++++++--
inc/helpers/renderFields.php | 45
assets/css/forms.min.css | 2
build/drawer-menu/style-index.css | 1
inc/blocks/_setup.php | 3
inc/helpers/media.php | 1
build/forms/view.asset.php | 2
inc/meta/MetaForm.php | 5
inc/rest/routes/UploadRoutes.php | 11
assets/js/concise/HandleSelectionOld.js | 392 +
build/drawer-menu/block.json | 27
inc/helpers/directory.php | 53
assets/css/nav.min.css | 2
build/drawer-menu/index.asset.php | 1
assets/js/min/uploader.min.js | 2
inc/managers/ScriptLoader.php | 1
src/drawer-menu/block.json | 27
src/drawer-menu/view.js | 1
build/feed/view.asset.php | 2
assets/js/concise/DataStore.js | 96
src/drawer-menu/edit.js | 33
src/drawer-menu/render.php | 39
src/list/render.php | 2
assets/js/min/dataStore.min.js | 2
build/feed/view.js | 2
assets/css/dash.css | 1
src/drawer-menu/editor.scss | 0
package.json | 2
build/forms/view.js | 2
inc/integrations/Helcim.php | 8
assets/css/forms.css | 1
88 files changed, 10,890 insertions(+), 4,633 deletions(-)
diff --git a/JVBase.php b/JVBase.php
index 45fc12e..0b63b08 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -1,13 +1,16 @@
<?php
namespace JVBase;
+use JVBase\blocks\CustomBlocks;
use JVBase\integrations\BlueSky;
+use JVBase\managers\CacheManager;
use JVBase\managers\EmailManager;
use JVBase\managers\ErrorHandler;
use JVBase\managers\LoginManager;
use JVBase\managers\MagicLinkManager;
use JVBase\managers\OperationQueue;
use JVBase\managers\DashboardManager;
+use JVBase\managers\DirectoryManager;
use JVBase\managers\ReferralManager;
use JVBase\managers\RoleManager;
//use JVBase\managers\SchemaManager;
@@ -56,6 +59,7 @@
protected array $integrations = [];
protected array $blocks = [];
protected array $routes = [];
+ protected CustomBlocks $customBlocks;
protected array $serviceMap = [
'maps' => 'JVBase\integrations\GoogleMaps',
@@ -81,6 +85,7 @@
public function __construct()
{
+ $this->customBlocks = new CustomBlocks();
$this->managers = [
'errors' => new ErrorHandler(),
'queue' => new OperationQueue(),
@@ -133,6 +138,10 @@
$this->routes['term'] = new TermRoutes();
}
+ if (Features::forSite()->has('is_directory')) {
+ $this->managers['directory'] = new DirectoryManager();
+ }
+
if (jvbSiteHasDashboard()) {
$this->routes['error'] = new ErrorRoutes();
@@ -193,11 +202,15 @@
{
return array_merge(array_keys($this->content), array_keys($this->taxonomies));
}
- public function dashboard()
+ public function dashboard():DashboardManager|false
{
- return $this->managers['dash'];
+ return $this->managers['dash']??false;
}
- public function error()
+ public function directories():DirectoryManager|false
+ {
+ return $this->managers['directory']??false;
+ }
+ public function error():ErrorHandler
{
return $this->managers['errors'];
}
@@ -205,11 +218,11 @@
{
return $this->managers['file'];
}
- public function cache()
+ public function cache():CacheManager
{
return $this->managers['cache'];
}
- public function queue()
+ public function queue():OperationQueue
{
return $this->managers['queue'];
}
@@ -217,9 +230,9 @@
// {
// return $this->managers['forms'];
// }
- public function notification()
+ public function notification():NotificationManager|false
{
- return $this->managers['notifications'];
+ return $this->managers['notifications']??false;
}
public function routes($route):mixed
{
@@ -228,7 +241,7 @@
}
return false;
}
- public function roles()
+ public function roles():RoleManager
{
return $this->managers['roles'];
}
@@ -333,4 +346,9 @@
<?php
}
}
+
+ public function blocks():CustomBlocks|bool
+ {
+ return $this->customBlocks??false;
+ }
}
diff --git a/assets/css/dash.css b/assets/css/dash.css
new file mode 100644
index 0000000..d3d300f
--- /dev/null
+++ b/assets/css/dash.css
@@ -0,0 +1 @@
+.icon-git-merge{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMTE0YTMwLDMwLDAsMCwwLTI5LjIxLDIzLjE5bC00NC02LjI4YTEwLDEwLDAsMCwxLTYuMTgtMy4zOUw5MS4xOCw4My44M0EzMCwzMCwwLDEsMCw3NCw4NS40djg1LjJhMzAsMzAsMCwxLDAsMTIsMFY5Ni4yMmwzMy41MiwzOS4xMWEyMiwyMiwwLDAsMCwxMy42LDcuNDZsNDUuMzUsNi40OEEzMCwzMCwwLDEsMCwyMDgsMTE0Wk02Miw1NkExOCwxOCwwLDEsMSw4MCw3NCwxOCwxOCwwLDAsMSw2Miw1NlpNOTgsMjAwYTE4LDE4LDAsMSwxLTE4LTE4QTE4LDE4LDAsMCwxLDk4LDIwMFptMTEwLTM4YTE4LDE4LDAsMSwxLDE4LTE4QTE4LDE4LDAsMCwxLDIwOCwxNjJaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-squares-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsNDJINTZBMTQsMTQsMCwwLDAsNDIsNTZ2NDhhMTQsMTQsMCwwLDAsMTQsMTRoNDhhMTQsMTQsMCwwLDAsMTQtMTRWNTZBMTQsMTQsMCwwLDAsMTA0LDQyWm0yLDYyYTIsMiwwLDAsMS0yLDJINTZhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabTk0LTYySDE1MmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMDAsNDJabTIsNjJhMiwyLDAsMCwxLTIsMkgxNTJhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabS05OCwzNEg1NmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMTA0LDEzOFptMiw2MmEyLDIsMCwwLDEtMiwySDU2YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMlptOTQtNjJIMTUyYTE0LDE0LDAsMCwwLTE0LDE0djQ4YTE0LDE0LDAsMCwwLDE0LDE0aDQ4YTE0LDE0LDAsMCwwLDE0LTE0VjE1MkExNCwxNCwwLDAsMCwyMDAsMTM4Wm0yLDYyYTIsMiwwLDAsMS0yLDJIMTUyYTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-rows{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMTM4SDQ4YTE0LDE0LDAsMCwwLTE0LDE0djQwYTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMjA4LDEzOFptMiw1NGEyLDIsMCwwLDEtMiwySDQ4YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJIMjA4YTIsMiwwLDAsMSwyLDJaTTIwOCw1MEg0OEExNCwxNCwwLDAsMCwzNCw2NHY0MGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjA4LDUwWm0yLDU0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY2NGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-table{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBaTTM4LDExMEg4MnYzNkgzOFptNTYsMEgyMTh2MzZIOTRaTTIxOCw2MlY5OEgzOFY2MlpNMzgsMTkyVjE1OEg4MnYzNkg0MEEyLDIsMCwwLDEsMzgsMTkyWm0xNzgsMkg5NFYxNThIMjE4djM0QTIsMiwwLDAsMSwyMTYsMTk0WiIvPjwvc3ZnPg==');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-eye{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDUuNDgsMTI1LjU3Yy0uMzQtLjc4LTguNjYtMTkuMjMtMjcuMjQtMzcuODFDMjAxLDcwLjU0LDE3MS4zOCw1MCwxMjgsNTBTNTUsNzAuNTQsMzcuNzYsODcuNzZjLTE4LjU4LDE4LjU4LTI2LjksMzctMjcuMjQsMzcuODFhNiw2LDAsMCwwLDAsNC44OGMuMzQuNzcsOC42NiwxOS4yMiwyNy4yNCwzNy44QzU1LDE4NS40Nyw4NC42MiwyMDYsMTI4LDIwNnM3My0yMC41Myw5MC4yNC0zNy43NWMxOC41OC0xOC41OCwyNi45LTM3LDI3LjI0LTM3LjhBNiw2LDAsMCwwLDI0NS40OCwxMjUuNTdaTTEyOCwxOTRjLTMxLjM4LDAtNTguNzgtMTEuNDItODEuNDUtMzMuOTNBMTM0Ljc3LDEzNC43NywwLDAsMSwyMi42OSwxMjgsMTM0LjU2LDEzNC41NiwwLDAsMSw0Ni41NSw5NS45NEM2OS4yMiw3My40Miw5Ni42Miw2MiwxMjgsNjJzNTguNzgsMTEuNDIsODEuNDUsMzMuOTRBMTM0LjU2LDEzNC41NiwwLDAsMSwyMzMuMzEsMTI4QzIyNi45NCwxNDAuMjEsMTk1LDE5NCwxMjgsMTk0Wm0wLTExMmE0Niw0NiwwLDEsMCw0Niw0NkE0Ni4wNiw0Ni4wNiwwLDAsMCwxMjgsODJabTAsODBhMzQsMzQsMCwxLDEsMzQtMzRBMzQsMzQsMCwwLDEsMTI4LDE2MloiLz48L3N2Zz4=');}.icon-eye-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRMNjQuNDQsNjdjLTM3LjI4LDIxLjktNTMuMjMsNTctNTMuOTIsNTguNTdhNiw2LDAsMCwwLDAsNC44OGMuMzQuNzcsOC42NiwxOS4yMiwyNy4yNCwzNy44QzU1LDE4NS40Nyw4NC42MiwyMDYsMTI4LDIwNmExMjQuOTEsMTI0LjkxLDAsMCwwLDUyLjU3LTExLjI1bDIzLDI1LjI5YTYsNiwwLDAsMCw4Ljg4LTguMDhabTQ4LjYyLDcxLjMyLDQ1LDQ5LjUyYTM0LDM0LDAsMCwxLTQ1LTQ5LjUyWk0xMjgsMTk0Yy0zMS4zOCwwLTU4Ljc4LTExLjQyLTgxLjQ1LTMzLjkzQTEzNC41NywxMzQuNTcsMCwwLDEsMjIuNjksMTI4YzQuMjktOC4yLDIwLjEtMzUuMTgsNTAtNTEuOTFMOTIuODksOTguM2E0Niw0NiwwLDAsMCw2MS4zNSw2Ny40OGwxNy44MSwxOS42QTExMy40NywxMTMuNDcsMCwwLDEsMTI4LDE5NFptNi40LTk5LjRhNiw2LDAsMCwxLDIuMjUtMTEuNzksNDYuMTcsNDYuMTcsMCwwLDEsMzcuMTUsNDAuODcsNiw2LDAsMCwxLTUuNDIsNi41M2wtLjU2LDBhNiw2LDAsMCwxLTYtNS40NUEzNC4xLDM0LjEsMCwwLDAsMTM0LjQsOTQuNlptMTExLjA4LDM1Ljg1Yy0uNDEuOTItMTAuMzcsMjMtMzIuODYsNDMuMTJhNiw2LDAsMSwxLTgtOC45NEExMzQuMDcsMTM0LjA3LDAsMCwwLDIzMy4zMSwxMjhhMTM0LjY3LDEzNC42NywwLDAsMC0yMy44Ni0zMi4wN0MxODYuNzgsNzMuNDIsMTU5LjM4LDYyLDEyOCw2MmExMjAuMTksMTIwLjE5LDAsMCwwLTE5LjY5LDEuNiw2LDYsMCwxLDEtMi0xMS44M0ExMzEuMTIsMTMxLjEyLDAsMCwxLDEyOCw1MGM0My4zOCwwLDczLDIwLjU0LDkwLjI0LDM3Ljc2LDE4LjU4LDE4LjU4LDI2LjksMzcsMjcuMjQsMzcuODFBNiw2LDAsMCwxLDI0NS40OCwxMzAuNDVaIi8+PC9zdmc+');}.icon-columns{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsMzRINjRBMTQsMTQsMCwwLDAsNTAsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0aDQwYTE0LDE0LDAsMCwwLDE0LTE0VjQ4QTE0LDE0LDAsMCwwLDEwNCwzNFptMiwxNzRhMiwyLDAsMCwxLTIsMkg2NGEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMlpNMTkyLDM0SDE1MmExNCwxNCwwLDAsMC0xNCwxNFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRoNDBhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMTkyLDM0Wm0yLDE3NGEyLDIsMCwwLDEtMiwySDE1MmEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-caret-double-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTMxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTgwLDgwYTYsNiwwLDAsMS04LjQ4LDBsLTgwLTgwYTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDIwNy41MWw3NS43Ni03NS43NUE2LDYsMCwwLDEsMjEyLjI0LDEzMS43NlptLTg4LjQ4LDguNDhhNiw2LDAsMCwwLDguNDgsMGw4MC04MGE2LDYsMCwwLDAtOC40OC04LjQ4TDEyOCwxMjcuNTEsNTIuMjQsNTEuNzZhNiw2LDAsMCwwLTguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-caret-double-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDEyNy41MSwxMjgsNTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTQwLjI0LDEzMi4yNFptODAtOC40OC04MC04MGE2LDYsMCwwLDAtOC40OCw4LjQ4TDIwNy41MSwxMjhsLTc1Ljc1LDc1Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsODAtODBBNiw2LDAsMCwwLDIyMC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-door{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMjE4SDIwNlY0MGExNCwxNCwwLDAsMC0xNC0xNEg2NEExNCwxNCwwLDAsMCw1MCw0MFYyMThIMjRhNiw2LDAsMCwwLDAsMTJIMjMyYTYsNiwwLDAsMCwwLTEyWk02Miw0MGEyLDIsMCwwLDEsMi0ySDE5MmEyLDIsMCwwLDEsMiwyVjIxOEg2MlptMTA0LDkyYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE2NiwxMzJaIi8+PC9zdmc+');}.icon-book-bookmark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMjZINzJBMzAsMzAsMCwwLDAsNDIsNTZWMjI0YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEySDU0di0yYTE4LDE4LDAsMCwxLDE4LTE4SDIwOGE2LDYsMCwwLDAsNi02VjMyQTYsNiwwLDAsMCwyMDgsMjZaTTExOCwzOGg1MnY3OEwxNDcuNTksOTkuMmE2LDYsMCwwLDAtNy4yLDBMMTE4LDExNlptODQsMTQ4SDcyYTI5Ljg3LDI5Ljg3LDAsMCwwLTE4LDZWNTZBMTgsMTgsMCwwLDEsNzIsMzhoMzR2OTBhNiw2LDAsMCwwLDkuNiw0LjhMMTQ0LDExMS41bDI4LjQxLDIxLjNBNiw2LDAsMCwwLDE4MiwxMjhWMzhoMjBaIi8+PC9zdmc+');}.icon-faders{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzQsMTIwdjk2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptNjYsNzRhNiw2LDAsMCwwLTYsNnYxNmE2LDYsMCwwLDAsMTIsMFYyMDBBNiw2LDAsMCwwLDIwMCwxOTRabTI0LTMySDIwNlY0MGE2LDYsMCwwLDAtMTIsMFYxNjJIMTc2YTYsNiwwLDAsMCwwLDEyaDQ4YTYsNiwwLDAsMCwwLTEyWk01NiwxNjJhNiw2LDAsMCwwLTYsNnY0OGE2LDYsMCwwLDAsMTIsMFYxNjhBNiw2LDAsMCwwLDU2LDE2MlptMjQtMzJINjJWNDBhNiw2LDAsMCwwLTEyLDB2OTBIMzJhNiw2LDAsMCwwLDAsMTJIODBhNiw2LDAsMCwwLDAtMTJabTcyLTQ4SDEzNFY0MGE2LDYsMCwwLDAtMTIsMFY4MkgxMDRhNiw2LDAsMCwwLDAsMTJoNDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-robot{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNTBIMTM0VjE2YTYsNiwwLDAsMC0xMiwwVjUwSDU2QTMwLDMwLDAsMCwwLDI2LDgwVjE5MmEzMCwzMCwwLDAsMCwzMCwzMEgyMDBhMzAsMzAsMCwwLDAsMzAtMzBWODBBMzAsMzAsMCwwLDAsMjAwLDUwWm0xOCwxNDJhMTgsMTgsMCwwLDEtMTgsMThINTZhMTgsMTgsMCwwLDEtMTgtMThWODBBMTgsMTgsMCwwLDEsNTYsNjJIMjAwYTE4LDE4LDAsMCwxLDE4LDE4Wk03NCwxMDhhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsNzQsMTA4Wm04OCwwYTEwLDEwLDAsMSwxLDEwLDEwQTEwLDEwLDAsMCwxLDE2MiwxMDhabTIsMzBIOTJhMjYsMjYsMCwwLDAsMCw1Mmg3MmEyNiwyNiwwLDAsMCwwLTUyWm0tMjIsMTJ2MjhIMTE0VjE1MFpNNzgsMTY0YTE0LDE0LDAsMCwxLDE0LTE0aDEwdjI4SDkyQTE0LDE0LDAsMCwxLDc4LDE2NFptODYsMTRIMTU0VjE1MGgxMGExNCwxNCwwLDAsMSwwLDI4WiIvPjwvc3ZnPg==');}.icon-plugs-connected{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTkuNzZhNiw2LDAsMCwwLTguNDgsMEwxNzMuOTQsNzMuNTdsLTYuNzktNi43OGEzMCwzMCwwLDAsMC00Mi40MiwwTDEwMCw5MS41MWwtNy43Ni03Ljc1YTYsNiwwLDAsMC04LjQ4LDguNDhMOTEuNTEsMTAwLDY2Ljc5LDEyNC43M2EzMCwzMCwwLDAsMCwwLDQyLjQybDYuNzgsNi43OUwxOS43NiwyMjcuNzZhNiw2LDAsMSwwLDguNDgsOC40OGw1My44Mi01My44MSw2Ljc5LDYuNzhhMzAsMzAsMCwwLDAsNDIuNDIsMEwxNTYsMTY0LjQ5bDcuNzYsNy43NWE2LDYsMCwwLDAsOC40OC04LjQ4TDE2NC40OSwxNTZsMjQuNzItMjQuNzNhMzAsMzAsMCwwLDAsMC00Mi40MmwtNi43OC02Ljc5LDUzLjgxLTUzLjgyQTYsNiwwLDAsMCwyMzYuMjQsMTkuNzZabS0xMTMuNDUsMTYxYTE4LDE4LDAsMCwxLTI1LjQ2LDBMNzUuMjcsMTU4LjY3YTE4LDE4LDAsMCwxLDAtMjUuNDZMMTAwLDEwOC40OSwxNDcuNTEsMTU2Wm01Ny45NC01Ny45NEwxNTYsMTQ3LjUxLDEwOC40OSwxMDBsMjQuNzItMjQuNzNhMTgsMTgsMCwwLDEsMjUuNDYsMGwyMi4wNiwyMi4wNmExOCwxOCwwLDAsMSwwLDI1LjQ2Wk05MC40MywzNC4yM2E2LDYsMCwwLDEsMTEuMTQtNC40Nmw4LDIwYTYsNiwwLDEsMS0xMS4xNCw0LjQ2Wm0tNjQsNTkuNTRhNiw2LDAsMCwxLDcuOC0zLjM0bDIwLDhhNiw2LDAsMSwxLTQuNDYsMTEuMTRsLTIwLThBNiw2LDAsMCwxLDI2LjQzLDkzLjc3Wm0yMDMuMTQsNjguNDZhNiw2LDAsMCwxLTcuOCwzLjM0bC0yMC04YTYsNiwwLDAsMSw0LjQ2LTExLjE0bDIwLDhBNiw2LDAsMCwxLDIyOS41NywxNjIuMjNabS02NCw1OS41NGE2LDYsMCwxLDEtMTEuMTQsNC40NmwtOC0yMGE2LDYsMCwwLDEsMTEuMTQtNC40NloiLz48L3N2Zz4=');}.icon-user-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wk03MS40NCwxOThhNjYsNjYsMCwwLDEsMTEzLjEyLDAsODkuOCw4OS44LDAsMCwxLTExMy4xMiwwWk05NCwxMjBhMzQsMzQsMCwxLDEsMzQsMzRBMzQsMzQsMCwwLDEsOTQsMTIwWm05OS41MSw2OS42NGE3Ny41Myw3Ny41MywwLDAsMC00MC0zMS4zOCw0Niw0NiwwLDEsMC01MSwwLDc3LjUzLDc3LjUzLDAsMCwwLTQwLDMxLjM4LDkwLDkwLDAsMSwxLDEzMSwwWiIvPjwvc3ZnPg==');}.icon-password{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==');}
\ No newline at end of file
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index ae2a3ee..7435ee0 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin:0 auto 0 var(--btn_)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:var(--btn_)}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chipchip);padding:.5rem!important;min-width:0;min-height:var(--chipchip);width:var(--chipchip)}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}
\ No newline at end of file
diff --git a/assets/css/forms.css b/assets/css/forms.css
new file mode 100644
index 0000000..18f0891
--- /dev/null
+++ b/assets/css/forms.css
@@ -0,0 +1 @@
+.icon-copy{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+');}.icon-paragraph{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==');}.icon-text-h-one{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEydjk2YTYsNiwwLDAsMS0xMiwwVjEyMy4yMUwyMDMuMzMsMTMzYTYsNiwwLDAsMS02LjY2LTEwbDI0LTE2YTYsNiwwLDAsMSw5LjMzLDVaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-two{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTAsNTZWMTc2YTYsNiwwLDAsMS0xMiwwVjEyMkg0NnY1NGE2LDYsMCwwLDEtMTIsMFY1NmE2LDYsMCwwLDEsMTIsMHY1NGg5MlY1NmE2LDYsMCwwLDEsMTIsMFptOTAsMTQ2SDIwNEwyNDAsMTU0LjA1QTMwLDMwLDAsMSwwLDE4Ny43MSwxMjYsNiw2LDAsMSwwLDE5OSwxMzBhMTgsMTgsMCwwLDEsMTQuNDctMTEuODIsMTgsMTgsMCwwLDEsMTYuODcsMjguNjZMMTg3LjIsMjA0LjRBNiw2LDAsMCwwLDE5MiwyMTRoNDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-text-h-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTgwYTM0LDM0LDAsMCwxLTU4LjI5LDIzLjc5LDYsNiwwLDAsMSw4LjU4LTguMzlBMjIsMjIsMCwxLDAsMjEyLDE1OGE2LDYsMCwwLDEtNC45Mi05LjQ0TDIyOC40OCwxMThIMTkyYTYsNiwwLDAsMSwwLTEyaDQ4YTYsNiwwLDAsMSw0LjkxLDkuNDRsLTIyLjUyLDMyLjE4QTM0LjA2LDM0LjA2LDAsMCwxLDI0NiwxODBaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTAsNTZWMTc2YTYsNiwwLDAsMS0xMiwwVjEyMkg0NnY1NGE2LDYsMCwwLDEtMTIsMFY1NmE2LDYsMCwwLDEsMTIsMHY1NGg5MlY1NmE2LDYsMCwwLDEsMTIsMFpNMjU0LDE4NGE2LDYsMCwwLDEtNiw2SDIzOHYxOGE2LDYsMCwwLDEtMTIsMFYxOTBIMTc2YTYsNiwwLDAsMS00Ljc0LTkuNjhsNTYtNzJBNiw2LDAsMCwxLDIzOCwxMTJ2NjZoMTBBNiw2LDAsMCwxLDI1NCwxODRabS0yOC01NC41MUwxODguMjcsMTc4SDIyNloiLz48L3N2Zz4=');}.icon-text-h-five{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTgwYTM0LDM0LDAsMCwxLTM0LDM0LDMzLjYsMzMuNiwwLDAsMS0yNC4yOS05LjgsNiw2LDAsMCwxLDguNTgtOC40QTIxLjY1LDIxLjY1LDAsMCwwLDIxMiwyMDJhMjIsMjIsMCwwLDAsMC00NCwyMS42NSwyMS42NSwwLDAsMC0xNS43MSw2LjJBNiw2LDAsMCwxLDE4Ni4wOCwxNTlsOC00OGE2LDYsMCwwLDEsNS45Mi01aDQwYTYsNiwwLDAsMSwwLDEySDIwNS4wOGwtNSwzMEEzNiwzNiwwLDAsMSwyMTIsMTQ2LDM0LDM0LDAsMCwxLDI0NiwxODBaTTE0NCw1MGE2LDYsMCwwLDAtNiw2djU0SDQ2VjU2YTYsNiwwLDAsMC0xMiwwVjE3NmE2LDYsMCwwLDAsMTIsMFYxMjJoOTJ2NTRhNiw2LDAsMCwwLDEyLDBWNTZBNiw2LDAsMCwwLDE0NCw1MFoiLz48L3N2Zz4=');}.icon-text-h-six{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIsMTQ2YTM0LjUsMzQuNSwwLDAsMC01LjYuNDdsMTguNzUtMzEuMzlhNiw2LDAsMCwwLTEwLjMtNi4xNmwtMzIuMjUsNTQtLjIyLjQxQTM0LDM0LDAsMSwwLDIxMiwxNDZabTAsNTZhMjIsMjIsMCwxLDEsMjItMjJBMjIsMjIsMCwwLDEsMjEyLDIwMlpNMTUwLDU2VjE3NmE2LDYsMCwwLDEtMTIsMFYxMjJINDZ2NTRhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDB2NTRoOTJWNTZhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-text-italic{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNTZhNiw2LDAsMCwxLTYsNkgxNTYuMzJsLTQ0LDEzMkgxNDRhNiw2LDAsMCwxLDAsMTJINjRhNiw2LDAsMCwxLDAtMTJIOTkuNjhsNDQtMTMySDExMmE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTk4LDU2WiIvPjwvc3ZnPg==');}.icon-text-underline{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsMjI0YTYsNiwwLDAsMS02LDZINjRhNiw2LDAsMCwxLDAtMTJIMTkyQTYsNiwwLDAsMSwxOTgsMjI0Wm0tNzAtMjZhNjIuMDcsNjIuMDcsMCwwLDAsNjItNjJWNTZhNiw2LDAsMCwwLTEyLDB2ODBhNTAsNTAsMCwwLDEtMTAwLDBWNTZhNiw2LDAsMCwwLTEyLDB2ODBBNjIuMDcsNjIuMDcsMCwwLDAsMTI4LDE5OFoiLz48L3N2Zz4=');}.icon-text-strikethrough{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTY5LjQ1YzExLjI4LDYuOTIsMjAuNTUsMTcuMzgsMjAuNTUsMzQsMCwyNS4zNi0yNy44MSw0Ni02Miw0NnMtNjItMjAuNjQtNjItNDZhNiw2LDAsMCwxLDEyLDBjMCwxOC43NSwyMi40MywzNCw1MCwzNHM1MC0xNS4yNSw1MC0zNGMwLTE4LjIzLTE1LjQ2LTI2LjU5LTQwLjQ3LTM0SDQwYTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNNzYuMzMsMTAyYTYuMiw2LjIsMCwwLDAsMS44OC0uM0E2LDYsMCwwLDAsODIsOTQuMTMsMTkuNzQsMTkuNzQsMCwwLDEsODEuMTEsODhjMC0xOS4zOCwyMC4xNi0zNCw0Ni44OS0zNCwxOS41OCwwLDM1LjU2LDcuODEsNDIuNzQsMjAuODlhNiw2LDAsMCwwLDEwLjUyLTUuNzhDMTcxLjk0LDUyLjEzLDE1Miw0MiwxMjgsNDIsOTQuNDMsNDIsNjkuMTEsNjEuNzcsNjkuMTEsODhhMzEuNjIsMzEuNjIsMCwwLDAsMS41Miw5Ljg3QTYsNiwwLDAsMCw3Ni4zMywxMDJaIi8+PC9zdmc+');}.icon-list-dashes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg5NkE2LDYsMCwwLDEsOTAsNjRabTEyNiw1OEg5NmE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIOTZhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk01Niw1OEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMlptMCw2NEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMlptMCw2NEg0MGE2LDYsMCwwLDAsMCwxMkg1NmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-list-numbers{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-text-align-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNDZIMTY4YTYsNiwwLDAsMCwwLTEySDQwYTYsNiwwLDAsMCwwLDEyWm0xNzYsMjhINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWm0tNDgsNDBINDBhNiw2LDAsMCwwLDAsMTJIMTY4YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-text-align-center{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRaTTY0LDk4YTYsNiwwLDAsMCwwLDEySDE5MmE2LDYsMCwwLDAsMC0xMlptMTUyLDQwSDQwYTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlptLTI0LDQwSDY0YTYsNiwwLDAsMCwwLDEySDE5MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-text-align-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRaTTIxNiw5OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNDBINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWm0wLDQwSDg4YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=');}.icon-link{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=');}.icon-file-pdf{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTUyYTYsNiwwLDAsMS02LDZIMTkwdjIwaDE4YTYsNiwwLDAsMSwwLDEySDE5MHYxOGE2LDYsMCwwLDEtMTIsMFYxNTJhNiw2LDAsMCwxLDYtNmgzMkE2LDYsMCwwLDEsMjIyLDE1MlpNOTAsMTcyYTI2LDI2LDAsMCwxLTI2LDI2SDU0djEwYTYsNiwwLDAsMS0xMiwwVjE1MmE2LDYsMCwwLDEsNi02SDY0QTI2LDI2LDAsMCwxLDkwLDE3MlptLTEyLDBhMTQsMTQsMCwwLDAtMTQtMTRINTR2MjhINjRBMTQsMTQsMCwwLDAsNzgsMTcyWm04NCw4YTM0LDM0LDAsMCwxLTM0LDM0SDExMmE2LDYsMCwwLDEtNi02VjE1MmE2LDYsMCwwLDEsNi02aDE2QTM0LDM0LDAsMCwxLDE2MiwxODBabS0xMiwwYTIyLDIyLDAsMCwwLTIyLTIySDExOHY0NGgxMEEyMiwyMiwwLDAsMCwxNTAsMTgwWk00MiwxMTJWNDBBMTQsMTQsMCwwLDEsNTYsMjZoOTZhNiw2LDAsMCwxLDQuMjUsMS43Nmw1Niw1NkE2LDYsMCwwLDEsMjE0LDg4djI0YTYsNiwwLDAsMS0xMiwwVjk0SDE1MmE2LDYsMCwwLDEtNi02VjM4SDU2YTIsMiwwLDAsMC0yLDJ2NzJhNiw2LDAsMCwxLTEyLDBaTTE1OCw4MmgzNS41MkwxNTgsNDYuNDhaIi8+PC9zdmc+');}.icon-file-csv{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00NiwxODBjMCwxMi4xMyw4LjA3LDIyLDE4LDIyYTE2LjI0LDE2LjI0LDAsMCwwLDExLjY3LTUuMjgsNiw2LDAsMCwxLDguNjYsOC4zQTI4LjA2LDI4LjA2LDAsMCwxLDY0LDIxNGMtMTYuNTQsMC0zMC0xNS4yNS0zMC0zNHMxMy40Ni0zNCwzMC0zNGEyOC4wNiwyOC4wNiwwLDAsMSwyMC4zMyw5LDYsNiwwLDAsMS04LjY2LDguM0ExNi4yMywxNi4yMywwLDAsMCw2NCwxNThDNTQuMDcsMTU4LDQ2LDE2Ny44Niw0NiwxODBabTgxLjA1LTYuNzdjLTEwLjg2LTMuMTMtMTMuNDEtNC42OS0xMy03LjkxYTYuNTksNi41OSwwLDAsMSwyLjg4LTUuMDhjNS42LTMuNzksMTcuNjYtMS44MiwyMS40NS0uODRhNiw2LDAsMCwwLDMuMDYtMTEuNmMtMi0uNTMtMjAuMS01LTMxLjIxLDIuNDhhMTguNjEsMTguNjEsMCwwLDAtOC4wOCwxMy41NGMtMS44LDE0LjE5LDEyLjI2LDE4LjI1LDIxLjU3LDIwLjk0LDEyLjEyLDMuNSwxNC43OCw1LjMzLDE0LjIsOS43NmE2Ljg1LDYuODUsMCwwLDEtMyw1LjM0Yy01LjYxLDMuNzMtMTcuNDgsMS42NC0yMS4xOS42MkE2LDYsMCwwLDAsMTEwLjQ4LDIxMmE1OS40MSw1OS40MSwwLDAsMCwxNC42OCwyYzUuNDksMCwxMS41NC0uOTUsMTYuMzYtNC4xNGExOC44OSwxOC44OSwwLDAsMCw4LjMxLTEzLjgxQzE1MS44NCwxODAuMzksMTM2LjkyLDE3Ni4wOCwxMjcuMDUsMTczLjIyWm04My0yNi44OGE2LDYsMCwwLDAtNy42NywzLjYzTDE4OCwxOTAuMTUsMTczLjY1LDE1MGE2LDYsMCwxLDAtMTEuMyw0bDIwLDU2YTYsNiwwLDAsMCwxMS4zLDBsMjAtNTZBNiw2LDAsMCwwLDIxMCwxNDYuMzRaTTIxNCw4OHYyNGE2LDYsMCwxLDEtMTIsMFY5NEgxNTJhNiw2LDAsMCwxLTYtNlYzOEg1NmEyLDIsMCwwLDAtMiwydjcyYTYsNiwwLDEsMS0xMiwwVjQwQTE0LDE0LDAsMCwxLDU2LDI2aDk2YTYsNiwwLDAsMSw0LjI0LDEuNzZsNTYsNTZBNiw2LDAsMCwxLDIxNCw4OFptLTIwLjQ5LTZMMTU4LDQ2LjQ4VjgyWiIvPjwvc3ZnPg==');}.icon-file-doc{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01MiwxNDZIMzZhNiw2LDAsMCwwLTYsNnY1NmE2LDYsMCwwLDAsNiw2SDUyYTM0LDM0LDAsMCwwLDAtNjhabTAsNTZINDJWMTU4SDUyYTIyLDIyLDAsMCwxLDAsNDRabTE2OC4xNS01LjQ2YTYsNiwwLDAsMSwuMTgsOC40OEEyOC4wNiwyOC4wNiwwLDAsMSwyMDAsMjE0Yy0xNi41NCwwLTMwLTE1LjI1LTMwLTM0czEzLjQ2LTM0LDMwLTM0YTI4LjA2LDI4LjA2LDAsMCwxLDIwLjMzLDksNiw2LDAsMCwxLTguNjYsOC4zQTE2LjIzLDE2LjIzLDAsMCwwLDIwMCwxNThjLTkuOTMsMC0xOCw5Ljg3LTE4LDIyczguMDcsMjIsMTgsMjJhMTYuMjMsMTYuMjMsMCwwLDAsMTEuNjctNS4yOEE2LDYsMCwwLDEsMjIwLjE1LDE5Ni41NFpNMTI4LDE0NmMtMTYuNTQsMC0zMCwxNS4yNS0zMCwzNHMxMy40NiwzNCwzMCwzNCwzMC0xNS4yNSwzMC0zNFMxNDQuNTQsMTQ2LDEyOCwxNDZabTAsNTZjLTkuOTMsMC0xOC05Ljg3LTE4LTIyczguMDctMjIsMTgtMjIsMTgsOS44NywxOCwyMlMxMzcuOTMsMjAyLDEyOCwyMDJaTTQ4LDExOGE2LDYsMCwwLDAsNi02VjQwYTIsMiwwLDAsMSwyLTJoOTBWODhhNiw2LDAsMCwwLDYsNmg1MHYxOGE2LDYsMCwwLDAsMTIsMFY4OGE2LDYsMCwwLDAtMS43Ni00LjI0bC01Ni01NkE2LDYsMCwwLDAsMTUyLDI2SDU2QTE0LDE0LDAsMCwwLDQyLDQwdjcyQTYsNiwwLDAsMCw0OCwxMThaTTE1OCw0Ni40OCwxOTMuNTIsODJIMTU4WiIvPjwvc3ZnPg==');}.icon-file-txt{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCwxMThhNiw2LDAsMCwwLDYtNlY0MGEyLDIsMCwwLDEsMi0yaDkwVjg4YTYsNiwwLDAsMCw2LDZoNTB2MThhNiw2LDAsMCwwLDEyLDBWODhhNiw2LDAsMCwwLTEuNzYtNC4yNGwtNTYtNTZBNiw2LDAsMCwwLDE1MiwyNkg1NkExNCwxNCwwLDAsMCw0Miw0MHY3MkE2LDYsMCwwLDAsNDgsMTE4Wk0xNTgsNDYuNDgsMTkzLjUyLDgySDE1OFptLTUuMTIsMTA5TDEzNS4zNywxODBsMTcuNTEsMjQuNTFhNiw2LDAsMSwxLTkuNzYsN0wxMjgsMTkwLjMybC0xNS4xMiwyMS4xN2E2LDYsMCwwLDEtOS43Ni03TDEyMC42MywxODBsLTE3LjUxLTI0LjUxYTYsNiwwLDEsMSw5Ljc2LTdMMTI4LDE2OS42OGwxNS4xMi0yMS4xN2E2LDYsMCwwLDEsOS43Niw3Wk05MCwxNTJhNiw2LDAsMCwxLTYsNkg3MHY1MGE2LDYsMCwwLDEtMTIsMFYxNThINDRhNiw2LDAsMCwxLDAtMTJIODRBNiw2LDAsMCwxLDkwLDE1MlptMTI4LDBhNiw2LDAsMCwxLTYsNkgxOTh2NTBhNiw2LDAsMCwxLTEyLDBWMTU4SDE3MmE2LDYsMCwwLDEsMC0xMmg0MEE2LDYsMCwwLDEsMjE4LDE1MloiLz48L3N2Zz4=');}.icon-file-xls{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNTQsMjA4YTYsNiwwLDAsMS02LDZIMTIwYTYsNiwwLDAsMS02LTZWMTUyYTYsNiwwLDEsMSwxMiwwdjUwaDIyQTYsNiwwLDAsMSwxNTQsMjA4Wk05MS40OCwxNDcuMTFhNiw2LDAsMCwwLTguMzYsMS4zOUw2OCwxNjkuNjcsNTIuODgsMTQ4LjVhNiw2LDAsMSwwLTkuNzYsN0w2MC42MywxODAsNDMuMTIsMjA0LjVhNiw2LDAsMSwwLDkuNzYsN0w2OCwxOTAuMzFsMTUuMTIsMjEuMTZBNiw2LDAsMCwwLDg4LDIxNGE1LjkxLDUuOTEsMCwwLDAsMy40OC0xLjEyLDYsNiwwLDAsMCwxLjQtOC4zN0w3NS4zNywxODBsMTcuNTEtMjQuNTFBNiw2LDAsMCwwLDkxLjQ4LDE0Ny4xMVpNMTkxLDE3My4yMmMtMTAuODUtMy4xMy0xMy40MS00LjY5LTEzLTcuOTFhNi41OSw2LjU5LDAsMCwxLDIuODgtNS4wOGM1LjYtMy43OSwxNy42NS0xLjgzLDIxLjQ0LS44NGE2LDYsMCwwLDAsMy4wNy0xMS42Yy0yLS41NC0yMC4xLTUtMzEuMjEsMi40OGExOC42NCwxOC42NCwwLDAsMC04LjA4LDEzLjU0Yy0xLjgsMTQuMTksMTIuMjYsMTguMjUsMjEuNTcsMjAuOTQsMTIuMTIsMy41LDE0Ljc3LDUuMzMsMTQuMiw5Ljc2YTYuODUsNi44NSwwLDAsMS0zLDUuMzRjLTUuNjEsMy43My0xNy40OCwxLjY0LTIxLjE5LjYyQTYsNiwwLDAsMCwxNzQuNDcsMjEyYTU5LjQxLDU5LjQxLDAsMCwwLDE0LjY4LDJjNS40OSwwLDExLjU0LS45NSwxNi4zNi00LjE0YTE4Ljg5LDE4Ljg5LDAsMCwwLDguMzEtMTMuODFDMjE1LjgzLDE4MC4zOSwyMDAuOTEsMTc2LjA4LDE5MSwxNzMuMjJaTTQyLDExMlY0MEExNCwxNCwwLDAsMSw1NiwyNmg5NmE2LDYsMCwwLDEsNC4yNCwxLjc2bDU2LDU2QTYsNiwwLDAsMSwyMTQsODh2MjRhNiw2LDAsMSwxLTEyLDBWOTRIMTUyYTYsNiwwLDAsMS02LTZWMzhINTZhMiwyLDAsMCwwLTIsMnY3MmE2LDYsMCwxLDEtMTIsMFpNMTU4LDgySDE5My41TDE1OCw0Ni40OFoiLz48L3N2Zz4=');}.icon-text-b-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjgsMTU2YTIwLDIwLDAsMCwxLTIwLDIwSDk2VjEzNmg1MkEyMCwyMCwwLDAsMSwxNjgsMTU2Wk0yMjQsNDhWMjA4YTE2LDE2LDAsMCwxLTE2LDE2SDQ4YTE2LDE2LDAsMCwxLTE2LTE2VjQ4QTE2LDE2LDAsMCwxLDQ4LDMySDIwOEExNiwxNiwwLDAsMSwyMjQsNDhaTTE4NCwxNTZhMzYsMzYsMCwwLDAtMTgtMzEuMTVBMzYsMzYsMCwwLDAsMTQwLDY0SDg4YTgsOCwwLDAsMC04LDhWMTg0YTgsOCwwLDAsMCw4LDhoNjBBMzYsMzYsMCwwLDAsMTg0LDE1NlptLTI0LTU2YTIwLDIwLDAsMCwwLTIwLTIwSDk2djQwaDQ0QTIwLDIwLDAsMCwwLDE2MCwxMDBaIi8+PC9zdmc+');}
\ No newline at end of file
diff --git a/assets/css/forms.min.css b/assets/css/forms.min.css
index bc9ce27..d108d0a 100644
--- a/assets/css/forms.min.css
+++ b/assets/css/forms.min.css
@@ -1 +1 @@
-input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group{margin-bottom:0}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .group-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:var(--btn_);right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--btn_);bottom:var(--btn_);left:1rem;right:1rem;border-radius:var(--radius-outer);padding:1rem;z-index:var(--z-7);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
+input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]),textarea{font-family:var(--body);font-size:var(--txt-medium);color:var(--contrast);padding:var(--p-y) var(--p-x);border-radius:var(--radius);background-color:var(--base);outline:0;border:1px solid var(--base-100);border-bottom:2px solid var(--contrast-200);width:100%;max-width:100%;margin:0 4px}input:is([type=date],[type=number],[type=text],[type=url],[type=email],[type=tel],[type=password],[type=search],[type=datetime-local],[type=time]):focus,textarea:focus{outline:var(--action-50);background-color:var(--base-100);color:var(--contrast)}input::placeholder,textarea::placeholder{font-family:var(--body);color:var(--base-200)}@media (min-width:768px){:root{--p-y:1rem}}select{background:var(--base);border:2px solid var(--base-100);border-radius:var(--radius);color:var(--contrast);cursor:pointer;font-family:var(--body);font-size:var(--txt-small);padding:.5rem 1rem;width:100%}select:disabled{background-color:var(--base-50);border-color:var(--base-100);color:var(--base-200);cursor:not-allowed}select option{background:var(--base);color:var(--contrast);padding:.5rem}select option:active,select option:checked,select option:focus,select option:hover{background:var(--action-0);color:var(--base);box-shadow:0 0 0 100px var(--action-0) inset}select option:checked{background:var(--action-0) linear-gradient(0deg,var(--action-0) 0,var(--action-0) 100%);color:var(--base)}select:hover{border-color:var(--action-0)}select:focus{border-color:var(--action-0)}input[type=search]:focus+.clear-search{opacity:1;cursor:pointer}.search-container .clear-search{opacity:0;cursor:default}.search-container .icon.search{padding:4px 8px;color:var(--contrast-200);--w:3rem}input[type=search]::-moz-search-clear-button,input[type=search]::-ms-clear,input[type=search]::-ms-reveal,input[type=search]::search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:none;visibility:hidden}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration,input[type=search]::-webkit-search-results-button,input[type=search]::-webkit-search-results-decoration{-webkit-appearance:none}input[type=url]{background:var(--linkIcon);background-position:.5em;background-size:1em;background-repeat:no-repeat;padding-left:2em}.integration .label,label{text-transform:uppercase;font-weight:700;margin-bottom:.5rem;display:block}.field{margin:2rem 0;position:relative}.field:has(.has-tooltip) label{margin-left:2rem}legend{padding:0 1rem}.date-wrapper{position:relative;display:inline-block}input[type=date]{padding:8px 36px 8px 8px;border-radius:4px}input[type=date]::-webkit-calendar-picker-indicator{opacity:0;width:100%;height:100%;position:absolute;top:0;left:0;cursor:pointer}input[type=date]+.icon{--w:20px;position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none}input:is([type=time],[type=datetime-local],[type=date]){padding:.5rem;border:1px solid var(--contrast-200);border-radius:4px;font-size:14px;min-width:180px;background:var(--base);color:var(--contrast);cursor:pointer}.date-wrapper input[type=date]:focus,.datetime-wrapper input[type=datetime-local]:focus,.field-input-wrapper input:is([type=time],[type=datetime-local],[type=date]):focus,.time-wrapper input[type=time]:focus{border-color:var(--action-0);box-shadow:0 0 0 2px rgba(var(--action-rgb),.1)}.date-wrapper .icon,.datetime-wrapper .icon,.field-input-wrapper .icon,.time-wrapper .icon{width:18px;height:18px;background-color:var(--contrast);opacity:.7}.selected-items{--justify:flex-start;--gap:.5rem;margin-bottom:.5rem}.selected-item{padding:.25rem .5rem;margin:.125em;background:var(--base-100);border-radius:.25rem;font-size:var(--txt-medium);border:1px solid var(--base-200);position:relative}.remove-item{background:0 0;border:none;padding:.25rem;cursor:pointer;color:#666;border-radius:var(--radius);width:1.5em;height:1.5em}.remove-item .close{width:.5em;height:.5em}.remove-item:hover{color:var(--action-0);background:#fee}.clear-filters{margin-left:auto;border:1px solid var(--base-200)}[type=checkbox],[type=radio],input.ch{position:absolute;opacity:0;left:-200vw}[type=checkbox]+label,[type=radio]+label,input.ch+label{position:relative;cursor:pointer}[type=checkbox]+label:hover,[type=radio]+label:hover{color:var(--action-0)}[type=checkbox]+label::after,[type=checkbox]+label::before,[type=radio]+label::after,[type=radio]+label::before,input.ch+label::after,input.ch+label::before{content:'';position:absolute;top:50%}[type=checkbox]+label::after,[type=radio]+label::after,input.ch+label::after{left:5px;transform:translateY(-70%) rotate(45deg);width:5px;height:10px;border:solid var(--light-0);border-width:0 2px 2px 0;display:none}[type=checkbox]+label::before,[type=radio]+label::before,input.ch+label::before{left:0;transform:translateY(-50%);width:1rem;height:1rem;border:2px solid var(--contrast-200);background-color:var(--base);border-radius:var(--radius)}[type=checkbox]:hover+label::before,[type=radio]:hover+label::before,input.ch:hover+label::before{border-color:var(--action-200)}[type=checkbox]:checked+label::before,[type=radio]:checked+label::before,input.ch:checked+label::before{background-color:var(--action-0);border-color:var(--action-100)}[type=radio]:checked+label::before{border-radius:50%}[type=checkbox]:checked+label::after,input.ch:checked+label::after{display:block;left:5px;top:50%;transform:translateY(-70%) rotate(45deg);width:.35rem;height:.66rem;border:solid var(--light-0);border-width:0 2px 2px 0}[type=checkbox]:disabled+label,[type=radio]:disabled+label,input.ch:disabled+label{cursor:not-allowed;background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label:hover,[type=radio]:disabled+label:hover,input.ch:disabled+label:hover{background-color:var(--base-50);color:var(--base-200);border-color:var(--base-200)}[type=checkbox]:disabled+label::before,[type=radio]:disabled+label::before,input.ch:disabled+label::before{border-color:var(--base-200)}[type=checkbox]:not(.btn)+label,[type=radio]:not(.btn)+label,input.ch+label{flex:1;padding-left:2rem;transform-origin:top center;will-change:transform}.btn+label::after,.btn+label::before{display:none}.btn+label{--w:1.2em;border:1px solid var(--base-200);border-radius:var(--radius);min-width:2rem;min-height:2rem;margin:0;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;gap:.5rem;color:var(--contrast-200);opacity:.8}.radio-options.status label{padding:0 .5rem}.btn:checked+label{border-color:var(--contrast);color:var(--contrast);opacity:1}.btn+label:hover{color:var(--action-50);border-color:var(--action-50)}.btn[hidden]+label,input[hidden]+label{display:none!important}.checkbox-options{--gap:.5rem 2rem}.checkbox-options label{flex:unset!important}.radio-options{--gap:.125rem .5rem}.radio-options input:not(.ch)+label::before{display:none!important}.radio-options input:not(.ch)+label{flex:unset!important;padding:.25rem!important;border-radius:4px;border:1px solid var(--base-100);color:var(--contrast-200);font-weight:400;text-align:center}.radio-options input:not(.ch)+label:hover,.radio-options input:not(.ch):checked+label{border-color:var(--action-0);color:var(--action-0)}.quantity{margin:0;display:inline-flex;width:fit-content;align-items:center;justify-content:center;border:1px solid transparent;border-radius:4px;position:relative}.quantity:focus-within{border-color:var(--action-0)}.quantity label{margin:0;font-size:var(--txt-small)}.quantity button{background:var(--base);padding:0;width:38px;height:38px;z-index:0;position:relative;border:1px solid var(--base-200);color:var(--contrast-200)}.quantity button:hover:not(:disabled){color:var(--action-0);border-color:var(--action-0);background-color:var(--base)}.quantity button:active:not(:disabled){background-color:var(--action-0);color:var(--light-0);transform:scale(.95)}.quantity button:disabled{opacity:.5;cursor:not-allowed}.quantity input[type=number]{z-index:1;border:1px solid var(--base-200);background:var(--base);text-align:center;font-size:1.1rem;width:60px;height:48px;margin:0;padding:0!important;appearance:textfield}.quantity input[type=number]::-webkit-inner-spin-button,.quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.quantity input[type=number]:focus{background-color:var(--base-50)}.quantity button.increase{left:-2px;border-radius:0 4px 4px 0}.quantity button.decrease{right:-2px;border-radius:4px 0 0 4px}.items-container{margin:0;padding:0;width:100%}.create-new-term{margin-top:1rem;width:100%}.create-new-term .field,.create-new-term[open] summary{margin-bottom:1rem}.create-new-term .field{max-width:100%}#jvb-selector>.wrap{--wrap:nowrap;--justify:flex-start}#jvb-selector .items-wrap{width:100%}#jvb-selector .items-container{display:grid;grid-template-columns:repeat(auto-fit,minmax(1fr,100%))}.tab-content[hidden]{display:block!important;transform:scaleY(0);height:0;overflow:hidden}.tab-content[hidden]:focus-within{transform:scaleY(1);height:auto}nav.tabs h2{margin:0!important;line-height:1;font-size:var(--txt-medium);display:flex;color:var(--contrast);white-space:nowrap;gap:1rem}nav.tabs .active h2{color:var(--action-contrast)}nav.tabs button{padding:.75rem 1.5rem;border-radius:0;position:relative;border:2px solid var(--action-0)}nav.tabs>button:first-of-type{border-top-left-radius:var(--radius)}nav.tabs>button:last-of-type{border-top-right-radius:var(--radius)}.tabs>button:focus,.tabs>button:hover{background-color:var(--base-200)}.tabs>button::after{content:'';position:absolute;bottom:-2px;left:0;width:0;height:3px;background-color:var(--action-50);transition:width .3s}.tabs>button.active::after,.tabs>button:hover::after{width:100%}.tabs>button.active::after{background-color:var(--action-200)}.tabs>button.active{background-color:var(--action-0);color:var(--action-contrast)}.tabs>button.active:focus,.tabs>button.active:hover{background-color:var(--action-100)}.tab-content h2{display:none}details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--content)}}.empty-group,.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--op-1));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--txt-large)}.dragover,.empty-group,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--op-2));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.empty-group p,.file-upload-text{color:var(--contrast);margin:0}.empty-group p strong,.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.groups{grid-template-columns:repeat(1,1fr)}.item-grid.group{margin-bottom:0}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0;padding-left:var(--chipchip)}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--op-1))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--op-4));opacity:1}.item-grid.preview summary span{display:none}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .icon-star-fi,[type=radio].featured:checked+label .icon-star{display:none}[type=radio].featured+label .icon-star,[type=radio].featured:checked+label .icon-star-fi{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--radius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--trans-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--trans-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--radius);background-color:rgba(var(--action-rgb),var(--op-1))}.upload-group .selected .field{margin:0}.upload-group .selection-actions button{aspect-ratio:unset}.submit-uploads{position:fixed;bottom:0;right:var(--btn_);z-index:var(--z-6);height:var(--btn);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);border-radius:var(--radius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{order:-1;grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--radius);margin:10px 0;cursor:pointer;transition:all var(--trans-base);text-align:center;background-color:rgba(var(--action-rgb),var(--op-1))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-7);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--op-3))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--z-9);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--radius-outer);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--op-3));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--op-4)));transform:scale(1.04)}}.selection-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--btn);bottom:var(--btn);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--op-6));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{--wrap:nowrap;height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--op-6));z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--op-6))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--btn);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;--wrap:nowrap;max-height:calc(100vh - var(--btnbtn));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.item-grid.restore{grid-template-columns:repeat(1,1fr)}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--radius);border-bottom-right-radius:var(--radius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px rgba(var(--base-rgb),var(--op-6));color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--radius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--txt-small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--txt-small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--txt-small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--txt-small)}form nav.tabs button h2{font-size:var(--txt-small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--txt-small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{min-height:0;padding:.5rem}.success-message{color:var(--success);background-color:var(--successBack);border:1px solid var(--success);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--successBack);border:2px solid var(--success);padding:1.5rem;border-radius:var(--radius-outer);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error)}.error-message{color:var(--error);font-size:var(--txt-small);margin-top:.25rem;display:block}.form-error{background-color:var(--errorBack);border:1px solid var(--error);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.invite details{margin-bottom:1.5rem}.field.tag-list .tag-input-row{display:flex;gap:.5rem;align-items:flex-start;margin-bottom:1rem;flex-wrap:wrap}.field.tag-list .tag-input-row .field{flex:1;min-width:150px;margin:0}.field.tag-list .tag-input-row .add-tag-item{flex-shrink:0;white-space:nowrap;margin-top:calc(var(--txt-medium) + 1rem)}.field.tag-list .tag-items{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem;min-height:2rem}.field.tag-list .tag-item{background:var(--base-200);padding:.4rem .75rem;border-radius:4px;display:inline-flex;align-items:center;gap:.5rem;font-size:.9rem;line-height:1.2}.field.tag-list .tag-item:hover{background:var(--base-100)}.field.tag-list .tag-label{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field.tag-list .remove-tag{min-height:0;padding:.25rem;color:var(--contrast);transition:transform .2s;box-shadow:none}.field.tag-list .remove-tag:hover{transform:scale(1.2)}@media (max-width:768px){.field.tag-list .tag-input-row{flex-direction:column;align-items:stretch}.field.tag-list .tag-input-row .field{min-width:100%}}
\ No newline at end of file
diff --git a/assets/css/icons.css b/assets/css/icons.css
index e69de29..22be381 100644
--- a/assets/css/icons.css
+++ b/assets/css/icons.css
@@ -0,0 +1 @@
+.icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-cloud-slash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik01Mi40NCwzNkE2LDYsMCwwLDAsNDMuNTYsNDRsNDAuMTgsNDQuMmMtLjQ1Ljg3LS45LDEuNzUtMS4zMiwyLjY0QTYyLDYyLDAsMSwwLDcyLDIxNGg4OGE4NS4yMyw4NS4yMywwLDAsMCwzMi4zNS02LjNMMjAzLjU2LDIyMGE2LDYsMCwwLDAsOC44OC04LjA4Wk0xNjAsMjAySDcyYTUwLDUwLDAsMSwxLDUuOS05OS42NEE4Ni4yNSw4Ni4yNSwwLDAsMCw3NCwxMjhhNiw2LDAsMCwwLDEyLDAsNzMuOTIsNzMuOTIsMCwwLDEsNi40NC0zMC4ybDkxLjIyLDEwMC4zNEE3My42NSw3My42NSwwLDAsMSwxNjAsMjAyWm04Ni03NGE4NS44NSw4NS44NSwwLDAsMS0yMS44NSw1Ny4yNyw2LDYsMCwwLDEtNC40NywyLDYsNiwwLDAsMS00LjQ3LTEwLDc0LDc0LDAsMCwwLTk5LTEwOC45Miw2LDYsMCwxLDEtNy4xMS05LjY3QTg2LDg2LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-exclamation-mark{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTg1LjI3LDg1LjI3LDAsMCwxLTE3LjIsNTEuNiw2LDYsMCwxLDEtOS42LTcuMkE3NCw3NCwwLDEsMCw4NiwxMjhhNiw2LDAsMCwxLTEyLDAsODUuNTQsODUuNTQsMCwwLDEsMy45MS0yNS42NEE1MC42OCw1MC42OCwwLDAsMCw3MiwxMDJhNTAsNTAsMCwwLDAsMCwxMDBIOTZhNiw2LDAsMCwxLDAsMTJINzJBNjIsNjIsMCwxLDEsODIuNDMsOTAuODgsODYsODYsMCwwLDEsMjQ2LDEyOFptLTY2LjI0LDQzLjc2TDE1OCwxOTMuNTFWMTI4YTYsNiwwLDAsMC0xMiwwdjY1LjUxbC0yMS43Ni0yMS43NWE2LDYsMCwwLDAtOC40OCw4LjQ4bDMyLDMyYTYsNiwwLDAsMCw4LjQ4LDBsMzItMzJhNiw2LDAsMCwwLTguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-cloud-check{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMzYuMjQtOTQuMjRhNiw2LDAsMCwxLDAsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxNDQsMTUxLjUxbDQzLjc2LTQzLjc1QTYsNiwwLDAsMSwxOTYuMjQsMTA3Ljc2WiIvPjwvc3ZnPg==');}.icon-cloud-warning{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptLTYtNzRWODhhNiw2LDAsMCwxLDEyLDB2NDBhNiw2LDAsMCwxLTEyLDBabTE2LDM2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE3MCwxNjRaIi8+PC9zdmc+');}.icon-syncing{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PHBhdGggaWQ9InJlZnJlc2giIGQ9Ik0xNjAuMDQ3IDEyMi44NzVhMzAuNzg0IDMwLjc4NCAwIDAgMC0yMS43NSA4Ljc5N2MtMi44NDIgMy4wMDMtLjQ2NyA0Ljk3MSAxLjMxMiAzLjE1NiAxMS4wNDMtMTAuNzg2IDI4LjcxLTEwLjY4IDM5LjYyNS4yMzRsNy4yMDMgNy4yMDRoLTEyLjg3NWMtMy4zNDcuMDA4LTMuMTY1IDMuODc1IDAgMy44NzVoMTYuMTFjMi4wNjIgMCAyLjU0LTEuNDE4IDIuNTYyLTQuOTdsLjA5NC0xNC45MjFjLjAyLTMuMjktMy40MzctMy4xNjUtMy40MzcgMHYxMi44NmwtNy4yMDMtNy4xODhhMzAuNzY4IDMwLjc2OCAwIDAgMC0yMS42NDEtOS4wNDd6bS0yOS41OTQgMzkuNzk3Yy0yLjA2MiAwLTIuNTI0IDEuNDAyLTIuNTQ3IDQuOTUzbC0uMDk0IDE0LjkyMmMtLjAyIDMuMjkgMy40MjIgMy4xNjQgMy40MjIgMHYtMTIuODZsNy4yMDMgNy4yMDRjMTEuOTU2IDExLjk1NSAzMS4zMTIgMTIuMDY0IDQzLjQwNy4yNSAyLjg0Mi0zLjAwMy40NTEtNC45ODgtMS4zMjgtMy4xNzItMTEuMDQzIDEwLjc4Ni0yOC43MSAxMC42OC0zOS42MjUtLjIzNWwtNy4xODgtNy4yMDNoMTIuODZjMy4zNDctLjAwOCAzLjE2NS0zLjg2IDAtMy44NmgtMTYuMTF6Ii8+PHBhdGggZD0iTTE2MCA0NGE4NC4xMSA4NC4xMSAwIDAgMC03Ni40MSA0OS4xMkE2MC43MSA2MC43MSAwIDAgMCA3MiA5MmE2MCA2MCAwIDAgMCAwIDEyMGg4OGE4NCA4NCAwIDAgMCAwLTE2OFptMCAxNjBINzJhNTIgNTIgMCAxIDEgOC41NS0xMDMuM0E4My42NiA4My42NiAwIDAgMCA3NiAxMjhhNCA0IDAgMCAwIDggMCA3NiA3NiAwIDEgMSA3NiA3NloiLz48L3N2Zz4=');}.icon-cloud-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFptMjguMjQtODUuNzZMMTY4LjQ4LDEzNmwxOS43NiwxOS43NmE2LDYsMCwxLDEtOC40OCw4LjQ4TDE2MCwxNDQuNDhsLTE5Ljc2LDE5Ljc2YTYsNiwwLDAsMS04LjQ4LTguNDhMMTUxLjUyLDEzNmwtMTkuNzYtMTkuNzZhNiw2LDAsMCwxLDguNDgtOC40OEwxNjAsMTI3LjUybDE5Ljc2LTE5Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-share-fat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTA3Ljc2bC04MC04MEE2LDYsMCwwLDAsMTQ2LDMyVjc0LjJjLTU0LjQ4LDMuNTktMTIwLjM5LDU1LTEyNy45MywxMjAuNjZhMTAsMTAsMCwwLDAsMTcuMjMsOGgwQzQ2LjU2LDE5MC44NSw4NywxNTIuNiwxNDYsMTUwLjEzVjE5MmE2LDYsMCwwLDAsMTAuMjQsNC4yNGw4MC04MEE2LDYsMCwwLDAsMjM2LjI0LDEwNy43NlpNMTU4LDE3Ny41MlYxNDRhNiw2LDAsMCwwLTYtNmMtMjcuNzMsMC01NC43Niw3LjI1LTgwLjMyLDIxLjU1YTE5My4zOCwxOTMuMzgsMCwwLDAtNDAuODEsMzAuNjVjNC43LTI2LjU2LDIwLjE2LTUyLDQ0LTcyLjI3Qzk4LjQ3LDk3Ljk0LDEyNy4yOSw4NiwxNTIsODZhNiw2LDAsMCwwLDYtNlY0Ni40OUwyMjMuNTEsMTEyWiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-scribble{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjUsMTg4LjI0YTE2LjYzLDE2LjYzLDAsMCwwLDAsMjMuNTIsNiw2LDAsMSwxLTguNDgsOC40OCwyOC42MSwyOC42MSwwLDAsMSwwLTQwLjQ4bDkuMzctOS4zOGExNi42MywxNi42MywwLDAsMC0yMy41Mi0yMy41MWwtNjYuNzUsNjYuNzVhMjguNjMsMjguNjMsMCwwLDEtNDAuNDktNDAuNDlsOTguNzYtOTguNzVhMTYuNjMsMTYuNjMsMCwwLDAtMjMuNTItMjMuNTFMODIuODYsMTE3LjYyQTI4LjYzLDI4LjYzLDAsMCwxLDQyLjM3LDc3LjEzTDgzLjc1LDM1Ljc2YTYsNiwwLDEsMSw4LjQ5LDguNDhMNTAuODYsODUuNjJhMTYuNjMsMTYuNjMsMCwwLDAsMjMuNTIsMjMuNTFsNjYuNzUtNjYuNzVhMjguNjMsMjguNjMsMCwwLDEsNDAuNDksNDAuNDlMODIuODYsMTgxLjYyYTE2LjYzLDE2LjYzLDAsMCwwLDIzLjUyLDIzLjUxbDY2Ljc2LTY2Ljc1YTI4LjYzLDI4LjYzLDAsMCwxLDQwLjQ5LDQwLjQ5WiIvPjwvc3ZnPg==');}.icon-brackets-angle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04NS4wNiw0My4yMiwzMS4xMSwxMjhsNTQsODQuNzhhNiw2LDAsMCwxLTEuODQsOC4yOCw2LDYsMCwwLDEtOC4yOC0xLjg0bC01Ni04OGE2LDYsMCwwLDEsMC02LjQ0bDU2LTg4YTYsNiwwLDAsMSwxMC4xMiw2LjQ0Wm0xNTIsODEuNTYtNTYtODhhNiw2LDAsMSwwLTEwLjEyLDYuNDRMMjI0Ljg5LDEyOGwtNTMuOTUsODQuNzhhNiw2LDAsMCwwLDEuODQsOC4yOCw2LDYsMCwwLDAsOC4yOC0xLjg0bDU2LTg4QTYsNiwwLDAsMCwyMzcuMDYsMTI0Ljc4WiIvPjwvc3ZnPg==');}.icon-brain{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI0YTU0LjEzLDU0LjEzLDAsMCwwLTMyLTQ5LjMzVjcyYTQ2LDQ2LDAsMCwwLTg2LTIyLjY3QTQ2LDQ2LDAsMCwwLDQyLDcydjIuNjdhNTQsNTQsMCwwLDAsMCw5OC42M1YxNzZhNDYsNDYsMCwwLDAsODYsMjIuNjdBNDYsNDYsMCwwLDAsMjE0LDE3NnYtMi43QTU0LjA3LDU0LjA3LDAsMCwwLDI0NiwxMjRaTTg4LDIxMGEzNCwzNCwwLDAsMS0zNC0zMi45NEE1My42Nyw1My42NywwLDAsMCw2NCwxNzhoOGE2LDYsMCwwLDAsMC0xMkg2NEE0Miw0MiwwLDAsMSw1MCw4NC4zOWE2LDYsMCwwLDAsNC01LjY2VjcyYTM0LDM0LDAsMCwxLDY4LDB2NzMuMDVBNDUuODksNDUuODksMCwwLDAsODgsMTMwYTYsNiwwLDAsMCwwLDEyLDM0LDM0LDAsMCwxLDAsNjhabTEwNC00NGgtOGE2LDYsMCwwLDAsMCwxMmg4YTUzLjY3LDUzLjY3LDAsMCwwLDEwLS45NEEzNCwzNCwwLDEsMSwxNjgsMTQyYTYsNiwwLDAsMCwwLTEyLDQ1Ljg5LDQ1Ljg5LDAsMCwwLTM0LDE1LjA1VjcyYTM0LDM0LDAsMCwxLDY4LDB2Ni43M2E2LDYsMCwwLDAsNCw1LjY2QTQyLDQyLDAsMCwxLDE5MiwxNjZabTE0LTU0YTYsNiwwLDAsMS02LDZoLTRhMzQsMzQsMCwwLDEtMzQtMzRWODBhNiw2LDAsMCwxLDEyLDB2NGEyMiwyMiwwLDAsMCwyMiwyMmg0QTYsNiwwLDAsMSwyMDYsMTEyWk02MCwxMThINTZhNiw2LDAsMCwxLDAtMTJoNEEyMiwyMiwwLDAsMCw4Miw4NFY4MGE2LDYsMCwwLDEsMTIsMHY0QTM0LDM0LDAsMCwxLDYwLDExOFoiLz48L3N2Zz4=');}.icon-palette{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTkuMzcsNTUuMzFBMTAxLjMyLDEwMS4zMiwwLDAsMCwxMjgsMjZoLTFBMTAyLDEwMiwwLDAsMCwyNiwxMjhjMCw0Mi4wOSwyNi4wNyw3Ny40NCw2OCw5Mi4yNkEzMC4yMSwzMC4yMSwwLDAsMCwxMDQuMTEsMjIyLDMwLjA2LDMwLjA2LDAsMCwwLDEzNCwxOTJhMTgsMTgsMCwwLDEsMTgtMThoNDYuMjFhMjkuODIsMjkuODIsMCwwLDAsMjkuMjUtMjMuMzFBMTAyLjcxLDEwMi43MSwwLDAsMCwyMzAsMTI3LjExLDEwMS4yNSwxMDEuMjUsMCwwLDAsMTk5LjM3LDU1LjMxWk0yMTUuNzYsMTQ4YTE3Ljg5LDE3Ljg5LDAsMCwxLTE3LjU1LDE0SDE1MmEzMCwzMCwwLDAsMC0zMCwzMCwxOCwxOCwwLDAsMS0yNCwxN0M2MSwxOTUuODYsMzgsMTY0Ljg1LDM4LDEyOGE5MCw5MCwwLDAsMSw4OS4wNy05MEgxMjhhOTAuMzQsOTAuMzQsMCwwLDEsOTAsODkuMjJBOTAuNDYsOTAuNDYsMCwwLDEsMjE1Ljc2LDE0OFpNMTM4LDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCw3NlpNOTQsMTAwQTEwLDEwLDAsMSwxLDg0LDkwLDEwLDEwLDAsMCwxLDk0LDEwMFptMCw1NmExMCwxMCwwLDEsMS0xMC0xMEExMCwxMCwwLDAsMSw5NCwxNTZabTg4LTU2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE4MiwxMDBaIi8+PC9zdmc+');}.icon-pen-nib{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsOTIuNjhhMTMuOTQsMTMuOTQsMCwwLDAtNC4xLTkuOUwxNzMuMjEsMTQuMWExNCwxNCwwLDAsMC0xOS44LDBMMTI0LjY4LDQyLjgzLDY2LjIyLDY0Ljc2YTE0LDE0LDAsMCwwLTguOSwxMC44TDM0LjA4LDIxNUE2LDYsMCwwLDAsNDAsMjIyYTYuNjEsNi42MSwwLDAsMCwxLS4wOGwxMzkuNDQtMjMuMjRhMTQsMTQsMCwwLDAsMTAuODEtOC45bDIxLjkyLTU4LjQ2LDI4Ljc0LTI4Ljc0QTEzLjkyLDEzLjkyLDAsMCwwLDI0Niw5Mi42OFptLTY2LDkyLjg5YTIsMiwwLDAsMS0xLjU0LDEuMjdMNTcuNDksMjA3bDUyLjg3LTUyLjg4YTI2LDI2LDAsMSwwLTguNDgtOC40OEw0OSwxOTguNTNsMjAuMTctMTIxQTIsMiwwLDAsMSw3MC40Myw3Nmw1Ni4wNi0yMUwyMDEsMTI5LjUxWk0xMTAsMTMyYTE0LDE0LDAsMSwxLDE0LDE0QTE0LDE0LDAsMCwxLDExMCwxMzJaTTIzMy40MSw5NC4xLDIwOCwxMTkuNTEsMTM2LjQ4LDQ4LDE2MS45LDIyLjU4YTIsMiwwLDAsMSwyLjgzLDBsNjguNjgsNjguNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-city{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMjEwSDIzMFY4OGE2LDYsMCwwLDAtNi02SDE2MGE2LDYsMCwwLDAtNiw2djQySDEwMlY0MGE2LDYsMCwwLDAtNi02SDMyYTYsNiwwLDAsMC02LDZWMjEwSDE2YTYsNiwwLDAsMCwwLDEySDI0MGE2LDYsMCwwLDAsMC0xMlpNMTY2LDk0aDUyVjIxMEgxNjZabS0xMiw0OHY2OEgxMDJWMTQyWk0zOCw0Nkg5MFYyMTBIMzhaTTcwLDcyVjg4YTYsNiwwLDAsMS0xMiwwVjcyYTYsNiwwLDAsMSwxMiwwWm0wLDQ4djE2YTYsNiwwLDAsMS0xMiwwVjEyMGE2LDYsMCwwLDEsMTIsMFptMCw0OHYxNmE2LDYsMCwwLDEtMTIsMFYxNjhhNiw2LDAsMCwxLDEyLDBabTUyLDE2VjE2OGE2LDYsMCwwLDEsMTIsMHYxNmE2LDYsMCwwLDEtMTIsMFptNjQsMFYxNjhhNiw2LDAsMCwxLDEyLDB2MTZhNiw2LDAsMCwxLTEyLDBabTAtNDhWMTIwYTYsNiwwLDAsMSwxMiwwdjE2YTYsNiwwLDAsMS0xMiwwWiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-shapes{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik02OS42OSw2Mi4xYTYsNiwwLDAsMC0xMS4zOCwwbC00MCwxMjBBNiw2LDAsMCwwLDI0LDE5MGg4MGE2LDYsMCwwLDAsNS42OS03LjlaTTMyLjMyLDE3OCw2NCw4M2wzMS42OCw5NVpNMjA2LDc2YTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDIwNiw3NlptLTg4LDBhMzgsMzgsMCwxLDEsMzgsMzhBMzgsMzgsMCwwLDEsMTE4LDc2Wm0xMDYsNzBIMTM2YTYsNiwwLDAsMC02LDZ2NTZhNiw2LDAsMCwwLDYsNmg4OGE2LDYsMCwwLDAsNi02VjE1MkE2LDYsMCwwLDAsMjI0LDE0NlptLTYsNTZIMTQyVjE1OGg3NloiLz48L3N2Zz4=');}.icon-diamonds-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjMuNzYsMTA4LjI0YTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBhNiw2LDAsMCwwLDAtOC40OGwtNDAtNDBhNiw2LDAsMCwwLTguNDgsMGwtNDAsNDBhNiw2LDAsMCwwLDAsOC40OFpNMTI4LDMyLjQ5LDE1OS41MSw2NCwxMjgsOTUuNTEsOTYuNDksNjRabTQuMjQsMTE1LjI3YTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MGE2LDYsMCwwLDAsMC04LjQ4Wk0xMjgsMjIzLjUxLDk2LjQ5LDE5MiwxMjgsMTYwLjQ5LDE1OS41MSwxOTJabTEwOC4yNC05OS43NS00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsMCw4LjQ4bDQwLDQwYTYsNiwwLDAsMCw4LjQ4LDBsNDAtNDBBNiw2LDAsMCwwLDIzNi4yNCwxMjMuNzZaTTE5MiwxNTkuNTEsMTYwLjQ5LDEyOCwxOTIsOTYuNDksMjIzLjUxLDEyOFptLTgzLjc2LTM1Ljc1LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDBsLTQwLDQwYTYsNiwwLDAsMCwwLDguNDhsNDAsNDBhNiw2LDAsMCwwLDguNDgsMGw0MC00MEE2LDYsMCwwLDAsMTA4LjI0LDEyMy43NlpNNjQsMTU5LjUxLDMyLjQ5LDEyOCw2NCw5Ni40OSw5NS41MSwxMjhaIi8+PC9zdmc+');}.icon-crosshair-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE4NGE2LDYsMCwwLDAtMTIsMHYzMy44QTkwLjE1LDkwLjE1LDAsMCwxLDM4LjIsMTM0SDcyYTYsNiwwLDAsMCwwLTEySDM4LjJBOTAuMTUsOTAuMTUsMCwwLDEsMTIyLDM4LjJWNzJhNiw2LDAsMCwwLDEyLDBWMzguMkE5MC4xNSw5MC4xNSwwLDAsMSwyMTcuOCwxMjJIMTg0YTYsNiwwLDAsMCwwLDEyaDMzLjhBOTAuMTUsOTAuMTUsMCwwLDEsMTM0LDIxNy44WiIvPjwvc3ZnPg==');}.icon-circle-notch{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTI4YTEwMiwxMDIsMCwwLDEtMjA0LDBjMC00MC4xOCwyMy4zNS03Ni44Niw1OS41LTkzLjQ1YTYsNiwwLDAsMSw1LDEwLjlDNTguNjEsNjAuMDksMzgsOTIuNDksMzgsMTI4YTkwLDkwLDAsMCwwLDE4MCwwYzAtMzUuNTEtMjAuNjEtNjcuOTEtNTIuNS04Mi41NWE2LDYsMCwwLDEsNS0xMC45QzIwNi42NSw1MS4xNCwyMzAsODcuODIsMjMwLDEyOFoiLz48L3N2Zz4=');}.icon-cards-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsOTBINDhhMTQsMTQsMCwwLDAtMTQsMTR2OTZhMTQsMTQsMCwwLDAsMTQsMTRIMjA4YTE0LDE0LDAsMCwwLDE0LTE0VjEwNEExNCwxNCwwLDAsMCwyMDgsOTBabTIsMTEwYTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlYxMDRhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMlpNNTAsNjRhNiw2LDAsMCwxLDYtNkgyMDBhNiw2LDAsMCwxLDAsMTJINTZBNiw2LDAsMCwxLDUwLDY0Wk02NiwzMmE2LDYsMCwwLDEsNi02SDE4NGE2LDYsMCwwLDEsMCwxMkg3MkE2LDYsMCwwLDEsNjYsMzJaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTkyYTYsNiwwLDAsMS02LDZIODhhNiw2LDAsMCwxLTYtNlY2Mi40OUw0NC4yNCwxMDAuMjRhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4YTYsNiwwLDEsMS04LjQ4LDguNDhMOTQsNjIuNDlWMTg2SDIzMkE2LDYsMCwwLDEsMjM4LDE5MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMjQsMTAwLjI0YTYsNiwwLDAsMS04LjQ4LDBMMTc0LDYyLjQ5VjE5MmE2LDYsMCwwLDEtNiw2SDI0YTYsNiwwLDAsMSwwLTEySDE2MlY2Mi40OWwtMzcuNzYsMzcuNzVhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4QTYsNiwwLDAsMSwyMjAuMjQsMTAwLjI0WiIvPjwvc3ZnPg==');}.icon-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+');}.icon-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-minus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkg4OGE2LDYsMCwwLDEsMC0xMmg4MEE2LDYsMCwwLDEsMTc0LDEyOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-loading{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNSIgICAgaWQ9InN2ZzEwIiAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzICAgIGlkPSJkZWZzMTAiIC8+PHBhdGggICAgaWQ9InBhdGgxMSIgICAgc3R5bGU9ImJhc2VsaW5lLXNoaWZ0OmJhc2VsaW5lO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmVjdG9yLWVmZmVjdDpub25lO2ZpbGw6IzIyMjIyMjtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgZD0ibSAxNi42MjEwOTQsMS4xNDI1NzgxIGMgLTguMjY2MzIzMiwwIC0xNi4yMjA4NjczOCw2LjQ0MjgwOTUgLTE1LjU4NTkzNzgsMTQuNjg1NTQ2OSAwLjYwMTM0NTUsNy44MDczMDggNy40MzQxMjY0LDE0LjEyNjk4IDE0LjkzMzU5MzgsMTQuOTQzMzU5IDguODM5ODQ1LDAuOTYyMjgzIDE1LjUwNTQ2OSwtNi4zNzY5MTkgMTUuMDA1ODU5LC0xNC45ODYzMjggQyAzMC40OTU5LDcuNTM2MjY4NCAyNC44ODMzOTcsMS4xNDI1NzgxIDE2LjYyMTA5NCwxLjE0MjU3ODEgWiBtIDAsMC42NTAzOTA3IEMgMjYuNDg4Nzg2LDEuODAzODY0NSAyOS43MTQ1MTgsOS41OTM1ODMzIDMwLjMwMjczNCwxNS44MDQ2ODggMzEuMTQxOTgyLDI0LjY2NjM2NSAyMi4xNjA0NTksMzEuMTY4MDc3IDE2LjAzOTA2MiwzMC4xMjUgOC44OTUxMzI3LDI4LjkwNzY4MSAyLjI2MTMxNDIsMjMuMjc5Mzc2IDEuNjgzNTkzOCwxNS43NzkyOTcgMS4wNzY5MzM4LDcuOTAzMjc1NCA4LjcyMjU0NTEsMS43ODQyNjk5IDE2LjYyMTA5NCwxLjc5Mjk2ODggWiBtIC0wLjA2NDQ1LDEuMjE4NzUgYyAtMy42MTAwODMsMCAtNy4xNTQ3OTk1LDEuNDAxMDY4NyAtOS43MzA0NjkxLDMuNzAzMTI1IEMgNC4yNTA1MDIzLDkuMDE2OTAwMiAyLjY0MjAzNzIsMTIuMjI2Mjk1IDIuOTE5OTIxOSwxNS44MzM5ODQgMy40NDY5MzUsMjIuNjc1NzEyIDkuNDI4OTY0OSwyOC4xOTg5ODUgMTUuOTk4MDQ3LDI4LjkxNDA2MiAyMy43MTQyNTYsMjkuNzU0MDIzIDI5LjUzMTYwMywyMy4zMzE3IDI5LjA5NTcwMywxNS44MjAzMTIgMjguNjc3OTQ4LDguNjIxMzk1MyAyMy43NzY2ODYsMy4wMTE3MTg4IDE2LjU1NjY0MSwzLjAxMTcxODggWiBtIDAsMC4xOTUzMTI0IGMgNy4xMTkxMzQsMCAxMS45MzI3MSw1LjUwODEzNzMgMTIuMzQ1NzAzLDEyLjYyNDk5OTggQyAyOS4zMzIwNjIsMjMuMjM2ODk2IDIzLjYxODk1OCwyOS41NDU5OTggMTYuMDE5NTMxLDI4LjcxODc1IDkuNTQ1NDMyMSwyOC4wMTQwMTIgMy42MzQxNjM3LDIyLjU1NTE0MyAzLjExNTIzNDQsMTUuODE4MzU5IDIuODQyNDU2MywxMi4yNzY5NjcgNC40MTg0MTA5LDkuMTI4MzE2OSA2Ljk1NzAzMTIsNi44NTkzNzUgOS40OTU2NTE2LDQuNTkwNDMzMSAxMi45OTcwOTMsMy4yMDcwMzEyIDE2LjU1NjY0MSwzLjIwNzAzMTIgWiBtIC0wLjA3MDMxLDEuNDE2MDE1NyBjIC0zLjE2MTk3MywwIC02LjI2MzUwOSwxLjIyNTgxMzkgLTguNTE5NTMxMSwzLjI0MjE4NzUgQyA1LjcxMDc2OTEsOS44ODE2MDggNC4zMDE0NTQyLDEyLjY5NDU4OSA0LjU0NDkyMTksMTUuODU1NDY5IDUuMDA2NTYyNCwyMS44NDg1NTQgMTAuMjQ0MTc4LDI2LjY4NjE1OSAxNS45OTgwNDcsMjcuMzEyNSAyMi43NTcwMTMsMjguMDQ4MjYxIDI3Ljg1NDQ1MSwyMi40MjA5MzYgMjcuNDcyNjU2LDE1Ljg0MTc5NyAyNy4xMDY4MjQsOS41Mzc2MDI1IDIyLjgxMDE2LDQuNjIzMDQ2OSAxNi40ODYzMjgsNC42MjMwNDY5IFogbSAwLDAuMTk1MzEyNSBjIDYuMjIyOTIsMCAxMC40Mjk5NDYsNC44MTMwMTM4IDEwLjc5MTAxNiwxMS4wMzUxNTY2IDAuMzc1NjEzLDYuNDcyNjE1IC00LjYxNzU4NCwxMS45ODY3MiAtMTEuMjU5NzY2LDExLjI2MzY3MiBDIDEwLjM1ODY4NSwyNi41MDExODYgNS4xOTE4MzgxLDIxLjcyNzk4NSA0LjczODI4MTIsMTUuODM5ODQ0IDQuNDk5OTIwMSwxMi43NDUyNjIgNS44NzY3MzE1LDkuOTk0OTc3OCA4LjA5NTcwMzEsOC4wMTE3MTg4IDEwLjMxNDY3NSw2LjAyODQ1OTUgMTMuMzc0ODksNC44MTgzNTk0IDE2LjQ4NjMyOCw0LjgxODM1OTQgWiBtIC0wLjA2ODM2LDEuNDE2MDE1NiBjIC0yLjcxMzg3NywwIC01LjM3NjExOCwxLjA1MjUxNjQgLTcuMzEyNTAwMiwyLjc4MzIwMzEgLTEuOTM2MzgyOCwxLjczMDY4NjkgLTMuMTQ2NTUxNyw0LjE0NTMxMTkgLTIuOTM3NSw2Ljg1OTM3NDkgMC4zOTYyNjk5LDUuMTQ0NDMgNC44ODk0NDQyLDkuMjk0NDI5IDkuODI4MTI1Miw5LjgzMjAzMSA1LjgwMTc0OSwwLjYzMTU2MiAxMC4xNzkyNTcsLTQuMTk4ODI4IDkuODUxNTYyLC05Ljg0NTcwMyBDIDI1LjUzMzc1LDEwLjQ1MzgyMiAyMS44NDU2MTYsNi4yMzQzNzUgMTYuNDE3OTc0LDYuMjM0Mzc1IFogbSAwLDAuMTk1MzEyNSBjIDUuMzI2NzMsMCA4LjkyNTIyNiw0LjExNzkwNTUgOS4yMzQzNzUsOS40NDUzMTI1IDAuMzIxNTEzLDUuNTQwMzUxIC0zLjk0OTgwMSwxMC4yNTk0NzQgLTkuNjM0NzY2LDkuNjQwNjI1IEMgMTEuMTczODc1LDI0Ljk4ODM2MiA2Ljc0OTUxNDMsMjAuOTAwODE0IDYuMzYxMzI4MSwxNS44NjEzMjggNi4xNTczODMxLDEzLjIxMzU2MyA3LjMzNTA0MzEsMTAuODU5NjgyIDkuMjM0Mzc1LDkuMTYyMTA5NCAxMS4xMzM3MDcsNy40NjQ1MzcyIDEzLjc1NDYyOCw2LjQyOTY4NzUgMTYuNDE3OTY5LDYuNDI5Njg3NSBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTIuMjY1Nzc1LDAgLTQuNDg4NzI5LDAuODc5MjE5NiAtNi4xMDU0NjgsMi4zMjQyMTg5IC0xLjYxNjc0MDgsMS40NDQ5OTkgLTIuNjI3NzYwNywzLjQ2MTI2OSAtMi40NTMxMjU0LDUuNzI4NTE2IDAuMzMwODk4Niw0LjI5NTc2OCA0LjA4MTU5NjQsNy43NjAxMiA4LjIwNTA3ODQsOC4yMDg5ODQgNC44NDQ1MjUsMC41MjczNiA4LjUwMDE1NiwtMy41MDYwOTcgOC4yMjY1NjIsLTguMjIwNzAzIEMgMjMuOTYwNjcyLDExLjM3MTk5NiAyMC44ODEwNiw3Ljg0NTcwMzEgMTYuMzQ5NjE0LDcuODQ1NzAzMSBaIG0gMCwwLjE5NTMxMjUgYyA0LjQzMDUzNCwwIDcuNDIyNDYxLDMuNDIyNzk5NCA3LjY3OTY4OCw3Ljg1NTQ2ODQgMC4yNjc0MTIsNC42MDgwODIgLTMuMjgzOTc4LDguNTMyMjI2IC04LjAxMTcxOSw4LjAxNzU3OCBDIDExLjk4OTA3NSwyMy40NzU1MzggOC4zMDcxODk5LDIwLjA3NTU5MyA3Ljk4NDM3NSwxNS44ODQ3NjYgNy44MTQ4NDYzLDEzLjY4MzgxOSA4Ljc5NTMxMDUsMTEuNzI2MzM4IDEwLjM3NSwxMC4zMTQ0NTMgMTEuOTU0Njg5LDguOTAyNTY4OSAxNC4xMzQzNyw4LjA0MTAxNTYgMTYuMzQ5NjA5LDguMDQxMDE1NiBaIG0gLTAuMDY4MzYsMS40MTYwMTU2IGMgLTEuODE3NjcyLDAgLTMuNjAxMzQyLDAuNzAzOTY4OCAtNC44OTg0MzgsMS44NjMyODA4IC0xLjI5NzA5NSwxLjE1OTMxIC0yLjEwODk2ODMsMi43NzkxODUgLTEuOTY4NzQ5NSw0LjU5OTYxIDAuMjY1NTI2OSwzLjQ0NzExMSAzLjI3Mzc1MDUsNi4yMjU4MTMgNi41ODIwMzE1LDYuNTg1OTM3IDMuODg3Mjk1LDAuNDIzMTYgNi44MjMwMDgsLTIuODE1MzE4IDYuNjAzNTE1LC02LjU5NzY1NiBDIDIyLjM4OTU0MSwxMi4yODgyMjIgMTkuOTE2NDk1LDkuNDU3MDMxMSAxNi4yODEyNSw5LjQ1NzAzMTIgWiBtIDAsMC4xOTUzMTI2IGMgMy41MzQzMzMsMCA1LjkxNzc0MiwyLjcyNzY5NjIgNi4xMjMwNDcsNi4yNjU2MjUyIDAuMjEzMzExLDMuNjc1ODE0IC0yLjYxNjIwOCw2LjgwMzAyNSAtNi4zODY3MTksNi4zOTI1NzggLTMuMjEzMjk4LC0wLjM0OTc4NSAtNi4xNTA3NTk3LC0zLjA2MjEzIC02LjQwODIwMywtNi40MDQyOTcgLTAuMTM1MTEyMiwtMS43NTQxMjcgMC42NDQyNTIsLTMuMzEzMjU3IDEuOTA0Mjk3LC00LjQzOTQ1MyAxLjI2MDA0NSwtMS4xMjYxOTYgMy4wMDA0NDEsLTEuODE0NDUzMyA0Ljc2NzU3OCwtMS44MTQ0NTMyIHogbSAtMC4wNzAzMSwxLjQxNjAxNTIgYyAtMS4zNjk1NzIsMCAtMi43MTIsMC41MzA2NzUgLTMuNjg5NDU0LDEuNDA0Mjk3IC0wLjk3NzQ1MywwLjg3MzYyMiAtMS41OTAxNzcsMi4wOTUxNDUgLTEuNDg0Mzc1LDMuNDY4NzUgMC4yMDAxNTYsMi41OTg0NTIgMi40NjU5LDQuNjg5NTUxIDQuOTU4OTg1LDQuOTYwOTM4IDIuOTMwMDcsMC4zMTg5NTggNS4xNDM5MDgsLTIuMTIyNTg3IDQuOTc4NTE1LC00Ljk3MjY1NiAtMC4xNTgxNDUsLTIuNzI1MjQ0IC0yLjAyNDYyMiwtNC44NjEzMjkgLTQuNzYzNjcxLC00Ljg2MTMyOSB6IG0gMCwwLjE5NTMxMyBjIDIuNjM4MTM1LDAgNC40MTQ5NzUsMi4wMzQ1NDQgNC41NjgzNTksNC42Nzc3MzQgMC4xNTkyMTEsMi43NDM1NDYgLTEuOTUwMzg2LDUuMDczODI0IC00Ljc2MzY3Miw0Ljc2NzU3OCAtMi4zOTgxMDIsLTAuMjYxMDQ3IC00LjU5MTEzMSwtMi4yODc3NDEgLTQuNzgzMjAzLC00Ljc4MTI1IC0wLjEwMDY5NiwtMS4zMDczMDggMC40Nzk1MTksLTIuNDcwMDM5IDEuNDE5OTIyLC0zLjMxMDU0NiAwLjk0MDQwMywtMC44NDA1MDggMi4yMzk1NTcsLTEuMzUzNTE2IDMuNTU4NTk0LC0xLjM1MzUxNiB6IG0gLTAuMDY4MzYsMS40MTYwMTYgYyAtMC45MjE0NzIsMCAtMS44MjI2NTcsMC4zNTU0MjUgLTIuNDgwNDY5LDAuOTQzMzU5IC0wLjY1NzgxMSwwLjU4NzkzNCAtMS4wNzMzMzksMS40MTUwMSAtMS4wMDE5NTMsMi4zNDE3OTcgMC4xMzQ3ODUsMS43NDk3OTIgMS42NTYwOTUsMy4xNTMyOTEgMy4zMzM5ODUsMy4zMzU5MzcgMS45NzI4NDYsMC4yMTQ3NTkgMy40NjY3NiwtMS40MzE4MDkgMy4zNTU0NjgsLTMuMzQ5NjA5IC0wLjEwNjIyNCwtMS44MzA1MDMgLTEuMzY0MTc3LC0zLjI3MTQ4NyAtMy4yMDcwMzEsLTMuMjcxNDg0IHogbSAwLDAuMTk1MzEyIGMgMS43NDE5NDIsMCAyLjkxMjIwOSwxLjMzOTQ0IDMuMDEzNjcyLDMuMDg3ODkxIDAuMTA1MTEsMS44MTEyNzYgLTEuMjg0NTYyLDMuMzQ2NTc3IC0zLjE0MDYyNSwzLjE0NDUzMSAtMS41ODI5MDcsLTAuMTcyMzA3IC0zLjAzMzQ1NSwtMS41MTMzNTUgLTMuMTYwMTU2LC0zLjE1ODIwMyAtMC4wNjYyOCwtMC44NjA0OSAwLjMxNDc4NSwtMS42MjQ4NjggMC45MzU1NDcsLTIuMTc5Njg4IDAuNjIwNzQ5LC0wLjU1NDgxOSAxLjQ4MDYyLC0wLjg5NDUzMSAyLjM1MTU1NiwtMC44OTQ1MzEgeiBtIC0wLjA2ODM2LDEuNDE2MDE2IGMgLTAuNDczMzY5LDAgLTAuOTM1MjcxLDAuMTgyMTI5IC0xLjI3MzQzOCwwLjQ4NDM3NSAtMC4zMzgxNjcsMC4zMDIyNDYgLTAuNTU0NTQ2LDAuNzMwOTY5IC0wLjUxNzU3OCwxLjIxMDkzNyAwLjA2OTQxLDAuOTAxMTMzIDAuODQ4MjQ5LDEuNjE4OTgxIDEuNzEwOTM4LDEuNzEyODkxIDEuMDE1NjE2LDAuMTEwNTU3IDEuNzg5NjE0LC0wLjc0MTAzMSAxLjczMjQyMSwtMS43MjY1NjMgLTAuMDU0MywtMC45MzU3NjYgLTAuNzA1NjkxLC0xLjY4MTY0IC0xLjY1MjM0MywtMS42ODE2NCB6IG0gMCwwLjE5NTMxMiBjIDAuODQ1NzQsMCAxLjQwNzQ5LDAuNjQ0MzMzIDEuNDU3MDMxLDEuNDk4MDQ3IDAuMDUxMDEsMC44NzkwMDggLTAuNjE2NzkzLDEuNjE5MzI5IC0xLjUxNTYyNSwxLjUyMTQ4NCAtMC43Njc3MDYsLTAuMDgzNTcgLTEuNDc1NzgsLTAuNzM4OTY3IC0xLjUzNzEwOSwtMS41MzUxNTYgLTAuMDMxODYsLTAuNDEzNjcxIDAuMTUwMDU1LC0wLjc3OTY5NyAwLjQ1MTE3MiwtMS4wNDg4MjggMC4zMDExMTYsLTAuMjY5MTMxIDAuNzIxNjk4LC0wLjQzNTU0NyAxLjE0NDUzMSwtMC40MzU1NDcgeiIgLz48L3N2Zz4=');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-logo{--icon:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgICB3aWR0aD0iMzIiICAgIGhlaWdodD0iMzIiICAgIHZpZXdCb3g9IjAgMCAzMiAzMiIgICAgdmVyc2lvbj0iMS4xIiAgICB4bWw6c3BhY2U9InByZXNlcnZlIiAgICBzdHlsZT0iY2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVjYXA6c3F1YXJlO3N0cm9rZS1saW5lam9pbjpiZXZlbDtzdHJva2UtbWl0ZXJsaW1pdDoxLjUiICAgIGlkPSJzdmcxMiIgICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcyAgICBpZD0iZGVmczEyIiAvPjxwYXRoICAgIGQ9Ik0gMTYsMi4wOTE3OTY5IEMgOC4yODYyNTM4LDIuMDkxNzk2OSAyLjAyNTM5MDcsOC4zMjE0NzYzIDIuMDI1MzkwNiwxNiAyLjAyNTM5MDYsMjMuNjc4NTI0IDguMjg2MjUzOCwyOS45MDgyMDMgMTYsMjkuOTA4MjAzIDIzLjcxMzc0NiwyOS45MDgyMDMgMjkuOTc0NjA5LDIzLjY3ODUyNCAyOS45NzQ2MDksMTYgMjkuOTc0NjA5LDguMzIxNDc2MyAyMy43MTM3NDYsMi4wOTE3OTY5IDE2LDIuMDkxNzk2OSBaIG0gMCwwLjYwNTQ2ODcgYyAwLjIwMzk4LDAgMC40MDU2ODMsMC4wMDY2NCAwLjYwNzQyMiwwLjAxNTYyNSAwLjE5NjUxNiwwLjAwODc1IDAuMzkxNzI4LDAuMDIxOTcyIDAuNTg1OTM3LDAuMDM5MDYzIC0wLjE5NDE2NiwtMC4wMTcwODEgLTAuMzg5NDY3LC0wLjAzMDMxNiAtMC41ODU5MzcsLTAuMDM5MDYzIFYgMjkuMjg3MTA5IGMgMC4xOTY0NywtMC4wMDg3IDAuMzkxNzcxLC0wLjAyMTk4IDAuNTg1OTM3LC0wLjAzOTA2IC0wLjE5NDIwOSwwLjAxNzA5IC0wLjM4OTQyMSwwLjAzMDMxIC0wLjU4NTkzNywwLjAzOTA2IEMgMTYuNDA1NjgzLDI5LjI5NjA5IDE2LjIwMzk4LDI5LjMwMjczNCAxNiwyOS4zMDI3MzQgOC42MTIxNjcsMjkuMzAyNzM0IDIuNjMwODU5NCwyMy4zNDk0MDggMi42MzA4NTk0LDE2IDIuNjMwODU5NCw4LjY1MDU5MTggOC42MTIxNjcxLDIuNjk3MjY1NiAxNiwyLjY5NzI2NTYgWiBtIDEuMjgxMjUsMC4wNjI1IGMgMC4yMTQ5ODYsMC4wMjAzMjkgMC40Mjg3NCwwLjA0NTg0NSAwLjY0MDYyNSwwLjA3NjE3MiAwLjIxMTQxMiwwLjAzMDI1OSAwLjQyMDgyMywwLjA2NTQ5NyAwLjYyODkwNiwwLjEwNTQ2ODggQyAxOC4zNDI4MiwyLjkwMTQ3NDYgMTguMTMzMTU5LDIuODY2MTcxOCAxNy45MjE4NzUsMi44MzU5Mzc1IDE3LjcwOTkxNSwyLjgwNTYwNjYgMTcuNDk2MzEsMi43ODAwOTU1IDE3LjI4MTI1LDIuNzU5NzY1NiBaIG0gMS4yODcxMDksMC4xODM1OTM4IGMgMC4yMTExMDcsMC4wNDA4NDEgMC40MTk1MzMsMC4wODgwODEgMC42MjY5NTMsMC4xMzg2NzE5IDAuMTc2MDc3LDAuMDQyOTQ2IDAuMzUwMTg2LDAuMDkwODIgMC41MjM0MzgsMC4xNDA2MjUgLTAuMTczMDk1LC0wLjA0OTczMSAtMC4zNDc1MjUsLTAuMDk3NzM4IC0wLjUyMzQzOCwtMC4xNDA2MjUgLTAuMjA3NDI4LC0wLjA1MDU3MSAtMC40MTU4NCwtMC4wOTc4NDcgLTAuNjI2OTUzLC0wLjEzODY3MTkgeiBtIDEuMjg3MTEsMC4zMTgzNTk0IGMgMC4xNDQ0MTEsMC4wNDMxODUgMC4yODczODQsMC4wODg4NTMgMC40Mjk2ODcsMC4xMzY3MTg3IC0wLjE0MjM0OCwtMC4wNDc4NTcgLTAuMjg1MjMxLC0wLjA5MzU0MyAtMC40Mjk2ODcsLTAuMTM2NzE4NyB6IG0gMC42MTcxODcsMC4yMDExNzE4IGMgMC4xNjc2NzIsMC4wNTkxOTEgMC4zMzM1MzcsMC4xMjE5MTcxIDAuNDk4MDQ3LDAuMTg3NSAtMC4xNjQ1NjEsLTAuMDY1NTc0IC0wLjMzMDMyNCwtMC4xMjgzMTg3IC0wLjQ5ODA0NywtMC4xODc1IHogbSAwLjU2MjUsMC4yMTI4OTA3IGMgMC4xNzYxNzgsMC4wNzEyOTcgMC4zNTExMDcsMC4xNDYxNDY0IDAuNTIzNDM4LDAuMjI0NjA5MyAtMC4xNzIyMTgsLTAuMDc4MzcxIC0wLjM0NzM4LC0wLjE1MzM5MTYgLTAuNTIzNDM4LC0wLjIyNDYwOTMgeiBtIC01LjUyNzM0MywwLjExOTE0MDYgLTAuMTIxMDk0LDAuMDA1ODYgQyA4Ljg5ODA2NTIsNC4xMTkzODMgMy43MjY1NjI1LDkuNDY0MjE4NSAzLjcyNjU2MjUsMTYgYyAwLDYuNTM1ODE3IDUuMTcxNTAyNSwxMS44ODA2NTIgMTEuNjYwMTU2NSwxMi4xOTkyMTkgbCAwLjEyMTA5NCwwLjAwNTkgMC4wMTE3MiwtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNTkgQyA5LjAzNjA5MjgsMjcuNjQ0NjY0IDMuOTY4NzUsMjIuNDA3OTY1IDMuOTY4NzUsMTYgMy45Njg3NSw5LjU5MjA3MjEgOS4wMzYwOTY0LDQuMzU1MzY5OCAxNS4zOTg0MzgsNC4wNDI5Njg3IGwgMC4xMjEwOTMsLTAuMDA1ODYgeiBtIDYuMTA5Mzc0LDAuMTMwODU5NCBjIDAuMTY0NjM1LDAuMDc1OTExIDAuMzI3MzY0LDAuMTU1OTgyOCAwLjQ4ODI4MiwwLjIzODI4MTIgLTAuMTYxNTE3LC0wLjA4MjU5NCAtMC4zMjMwMjEsLTAuMTYyMTIwMyAtMC40ODgyODIsLTAuMjM4MjgxMiB6IG0gMC41ODM5ODUsMC4yODcxMDkzIGMgMC4xNTIwMjYsMC4wNzkzIDAuMzAwNzE1LDAuMTYzMTY1MyAwLjQ0OTIxOSwwLjI0ODA0NjkgLTAuMTQ4MjE1LC0wLjA4NDY5MiAtMC4yOTc0OTcsLTAuMTY4OTEzIC0wLjQ0OTIxOSwtMC4yNDgwNDY5IHogbSAwLjU1NjY0LDAuMzA4NTkzOCBjIDAuMTM4NDkzLDAuMDgwODY2IDAuMjc0OTEzLDAuMTY0MzcwOCAwLjQxMDE1NywwLjI1IC0wLjEzNTA3OSwtMC4wODU0ODYgLTAuMjcxODM3LC0wLjE2OTI2MjkgLTAuNDEwMTU3LC0wLjI1IHogbSAwLjU0ODgyOSwwLjMzOTg0MzcgYyAwLjEyMzIwMSwwLjA4MDE1MSAwLjI0NDkwMywwLjE2MjA0OTcgMC4zNjUyMzQsMC4yNDYwOTM4IC0wLjEyMDM0OSwtMC4wODQwMiAtMC4yNDIwMTUsLTAuMTY1OTY2MyAtMC4zNjUyMzQsLTAuMjQ2MDkzOCB6IG0gMC41NDY4NzUsMC4zNzUgYyAwLjEwMzM0OCwwLjA3NDc3MSAwLjIwNTU2LDAuMTUwODk4NiAwLjMwNjY0LDAuMjI4NTE1NiAtMC4xMDEwNzEsLTAuMDc3NTc1IC0wLjIwMzMwMiwtMC4xNTM3ODUgLTAuMzA2NjQsLTAuMjI4NTE1NiB6IG0gLTguMzQ3NjU3LDAuMDcwMzEyIC0wLjEyMTA5MywwLjAwNzgxIEMgOS43MzUzODExLDUuNjMxODMwOCA1LjI0NDE0MDYsMTAuMjk4OTk4IDUuMjQ0MTQwNiwxNiBjIDAsNS43MDEwMzMgNC40OTExNDg1LDEwLjM2ODE3NCAxMC4xNDA2MjU0LDEwLjY4NTU0NyBsIDAuMTIxMDkzLDAuMDA3OCAwLjAxMzY3LC0wLjI0MjE4NyAtMC4xMjEwOTMsLTAuMDA3OCBDIDkuODc0NjI3MiwyNi4xMzMwNDQgNS40ODgyODE0LDIxLjU3Mzc1NyA1LjQ4ODI4MTIsMTYgYyAwLC01LjU3MzcyNCA0LjM4NjM0MiwtMTAuMTMzMDQ2MSA5LjkxMDE1NjgsLTEwLjQ0MzM1OTQgbCAwLjEyMTA5MywtMC4wMDc4MSB6IG0gOC44NDk2MSwwLjMxMDU0NjkgYyAwLjEwMDk1NiwwLjA4MDUyNyAwLjIwMDMwNCwwLjE2Mjc0MTEgMC4yOTg4MjgsMC4yNDYwOTM3IC0wLjA5ODcsLTAuMDgzNDc1IC0wLjE5NzY5MiwtMC4xNjU0NTM1IC0wLjI5ODgyOCwtMC4yNDYwOTM3IHogbSAwLjQ5MDIzNCwwLjQxMDE1NjIgYyAwLjA5NDk3LDAuMDgzNDYyIDAuMTg4NzEzLDAuMTY3ODI3MiAwLjI4MTI1LDAuMjUzOTA2MyAtMC4wOTIzLC0wLjA4NTgxMyAtMC4xODY1MzUsLTAuMTcwNjk0OSAtMC4yODEyNSwtMC4yNTM5MDYzIHogbSAwLjQ0MzM1OSwwLjQwODIwMzIgQyAyNS4zOTM4NzcsNi41MzYzNjA5IDI1LjQ5ODE0Nyw2LjYzODAzNCAyNS41OTk2MDksNi43NDIxODcgMjUuNDk3OTc5LDYuNjM3OTAyIDI1LjM5NDA1NCw2LjUzNjQ4MjggMjUuMjg5MDYyLDYuNDM1NTQ2NCBaIE0gMTUuNTA1ODU5LDYuODIwMzEyIDE1LjM4NDc2Niw2LjgyODEyMiBDIDEwLjU3NDc5Niw3LjE0Mzc4OTUgNi43NjM2NzE5LDExLjEzMzk2MyA2Ljc2MzY3MTksMTYgYyAwLDQuODY2MDM4IDMuODExMDMyMSw4Ljg1NjE4OCA4LjYyMTA5NDEsOS4xNzE4NzUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTU2MywtMC4yNDIxODcgLTAuMTIxMDkzLC0wLjAwNzggQyAxMC43MTUxLDI0LjYyMjE4OCA3LjAwNTg1OTQsMjAuNzM5NTMgNy4wMDU4NTk0LDE2IGMgMCwtNC43Mzk1MjkgMy43MDkyNTE2LC04LjYyMjIxNjggOC4zOTQ1MzE2LC04LjkyOTY4NzUgTCAxNS41MjE0ODQsNy4wNjI1IFogbSAxMC4yMTg3NSwwLjA1MjczNCBjIDAuMDkzNzcsMC4wOTg4ODYgMC4xODY2MjgsMC4xOTkwNjk0IDAuMjc3MzQ0LDAuMzAwNzgxMiBDIDI1LjkxMTE0Myw3LjA3MjAzNDUgMjUuODE4NDc5LDYuOTcyMDA3OSAyNS43MjQ2MDksNi44NzMwNDYgWiBtIDAuNDM5NDUzLDAuNDg2MzI4MSBjIDAuMDk0MzksMC4xMDk4MzY1IDAuMTg2NTM0LDAuMjIxMDkyNCAwLjI3NzM0NCwwLjMzMzk4NDQgLTAuMDkwNjksLTAuMTEyNzA0NCAtMC4xODMwODEsLTAuMjI0MzI0OSAtMC4yNzczNDQsLTAuMzMzOTg0NCB6IG0gMC4zNTM1MTYsMC40Mjk2ODc1IGMgMC4xMDE5NDcsMC4xMjkxNDA4IDAuMjAxNjQ3LDAuMjU5NjQ5NiAwLjI5ODgyOCwwLjM5MjU3ODEgQyAyNi43MTkwODUsOC4wNDg1NTI4IDI2LjYxOTY3OSw3LjkxODM1MiAyNi41MTc1NzgsNy43ODkwNjE2IFogTSAyNi45MTAxNTYsOC4zMTI1IGMgMC4wODU5NywwLjEyMDYyOTIgMC4xNzE5MTgsMC4yNDE2NzMzIDAuMjUzOTA2LDAuMzY1MjM0NCBDIDI3LjA4MjE2MSw4LjU1NDM0MzYgMjYuOTk2MDM1LDguNDMyOTY1NSAyNi45MTAxNTYsOC4zMTI1IFogbSAtMTEuNDA2MjUsMC4wMjM0MzcgLTAuMTIxMDkzLDAuMDA3ODEgQyAxMS40MTI3OTgsOC42NTcxMTA1IDguMjgzMjAzMSwxMS45NjkxODcgOC4yODMyMDMxLDE2IGMgMCw0LjAzMDgzNSAzLjEyOTQwNzksNy4zNDI5MTggNy4wOTk2MDk5LDcuNjU2MjUgbCAwLjEyMTA5MywwLjAwNzggMC4wMTk1MywtMC4yNDAyMzQgLTAuMTIxMDk0LC0wLjAwOTggQyAxMS41NTU2NjUsMjMuMTEwNTcyIDguNTI1MzkwNiwxOS45MDU0MDkgOC41MjUzOTA2LDE2IGMgMCwtMy45MDUzODcgMy4wMzAyNzA0LC03LjExMDQ1NTggNi44NzY5NTM0LC03LjQxNDA2MjUgbCAwLjEyMTA5NCwtMC4wMDk3NyB6IG0gMTEuNzg3MTEsMC41NDI5Njg4IGMgMC4wNjkwMywwLjEwODE3MDQgMC4xMzkxMzMsMC4yMTU5MTA0IDAuMjA1MDc4LDAuMzI2MTcxOCAtMC4wNjU5LC0wLjExMDE1MTMgLTAuMTM2MDk5LC0wLjIxODEwODQgLTAuMjA1MDc4LC0wLjMyNjE3MTggeiBtIDAuMzQ1NzAzLDAuNTcyMjY1NiBjIDAuMDUzMjcsMC4wOTM1NTkgMC4xMDcxNjIsMC4xODYyOTU0IDAuMTU4MjAzLDAuMjgxMjUgLTAuMDUxMTIsLTAuMDk1MDgyIC0wLjEwNDg0NCwtMC4xODc1Njg1IC0wLjE1ODIwMywtMC4yODEyNSB6IG0gLTEyLjEzNDc2NiwwLjM5ODQzOCAtMC4xMjEwOTQsMC4wMTM2NzIgQyAxMi4yNTE1MjcsMTAuMTczMDgxIDkuODAyNzM0NCwxMi44MDQ4NzMgOS44MDI3MzQ0LDE2IGMgMCwzLjE5NTE0NCAyLjQ0ODUxMjYsNS44MjY5MiA1LjU3ODEyNDYsNi4xMzY3MTkgbCAwLjEyMTA5NCwwLjAxMzY3IDAuMDIzNDQsLTAuMjQyMTg4IC0wLjEyMTA5NCwtMC4wMTE3MiBDIDEyLjM5NjI0NiwyMS41OTg3MjUgMTAuMDQ0OTIyLDE5LjA3MTM0OCAxMC4wNDQ5MjIsMTYgYyAwLC0zLjA3MTMyOSAyLjM1MTMyOCwtNS41OTg3MTkgNS4zNTkzNzUsLTUuODk2NDg0IGwgMC4xMjEwOTQsLTAuMDExNzIgeiBtIDEyLjQ3NDYwOSwwLjI0MDIzNDYgYyAwLjAzMDk1LDAuMDYyMDIgMC4wNjM3NSwwLjEyMjk4NiAwLjA5Mzc1LDAuMTg1NTQ3IC0wLjAyOTk1LC0wLjA2MjQzIC0wLjA2Mjg1LC0wLjEyMzY2NCAtMC4wOTM3NSwtMC4xODU1NDcgeiBtIDAuMjgzMjA0LDAuNTk5NjA5IGMgMC4wMjU0LDAuMDU4MDIgMC4wNTE1OCwwLjExNTM4NCAwLjA3NjE3LDAuMTczODI4IC0wLjAyNDU4LC0wLjA1ODQxIC0wLjA1MDc4LC0wLjExNTg0OSAtMC4wNzYxNywtMC4xNzM4MjggeiBtIDAuMjM0Mzc1LDAuNTc0MjE5IGMgMC4wMjY0MiwwLjA2OTAyIDAuMDU0NzgsMC4xMzc0NyAwLjA4MDA4LDAuMjA3MDMxIC0wLjAyNTM2LC0wLjA2OTcyIC0wLjA1MzU5LC0wLjEzNzg2MSAtMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0xMi45OTgwNDcsMC4xMDU0NjkgLTAuMTE5MTQxLDAuMDE1NjMgQyAxMy4wODk4MDUsMTEuNjg4NjA0IDExLjMyMjI2NiwxMy42NDE0OTMgMTEuMzIyMjY2LDE2IGMgMCwyLjM1ODUyMSAxLjc2NzE3Nyw0LjMxMTM4NyA0LjA1NDY4Nyw0LjYxNTIzNCBsIDAuMTE5MTQxLDAuMDE1NjMgMC4wMzMyLC0wLjI0MDIzNCBMIDE1LjQwODIsMjAuMzc1IEMgMTMuMjM4ODQ1LDIwLjA4Njg0NyAxMS41NjQ0NSwxOC4yMzc0NjMgMTEuNTY0NDUsMTYgYyAwLC0yLjIzNzQ0NyAxLjY3NDM5NSwtNC4wODY4NDYgMy44NDM3NSwtNC4zNzUgbCAwLjEyMTA5NCwtMC4wMTU2MyB6IE0gMjguODc1LDEyLjQxNjAxNiBjIDAuMDMwOCwwLjEwOTg3NSAwLjA2Mzc4LDAuMjE5MDczIDAuMDkxOCwwLjMzMDA3OCAtMC4wMjc5OSwtMC4xMTA4NDYgLTAuMDYxMDMsLTAuMjIwMzYgLTAuMDkxOCwtMC4zMzAwNzggeiBtIC0xMy4zODg2NzIsMC40Nzg1MTUgLTAuMTE3MTg3LDAuMDIzNDQgQyAxMy45MjgwNzYsMTMuMjA5NjMyIDEyLjgzOTg0NCwxNC40ODA2NTEgMTIuODM5ODQ0LDE2IGMgMCwxLjUxOTM1IDEuMDg3ODAyLDIuNzkwMzY1IDIuNTI5Mjk3LDMuMDgyMDMxIGwgMC4xMTcxODcsMC4wMjM0NCAwLjA0ODgzLC0wLjIzODI4MiAtMC4xMTkxNCwtMC4wMjM0NCBDIDE0LjA4NTM0MiwxOC41NzQ1MSAxMy4wODM5ODQsMTcuNDAzODUyIDEzLjA4Mzk4NCwxNiBjIDAsLTEuNDAzODUxIDEuMDAxMzYyLC0yLjU3NDUwNyAyLjMzMjAzMiwtMi44NDM3NSBsIDAuMTE5MTQsLTAuMDIzNDQgeiBNIDI5LDEyLjg5NjQ4NCBDIDI5LjAzODA2LDEzLjA1NTA3IDI5LjA3NTEyLDEzLjIxNDI2MiAyOS4xMDc0MjIsMTMuMzc1IDI5LjA3NTA2NywxMy4yMTQwNTQgMjkuMDM4MTMyLDEzLjA1NTI3MSAyOSwxMi44OTY0ODQgWiBtIDAuMTM4NjcyLDAuNjQ2NDg1IGMgMC4wMjk1NywwLjE1NzcwOCAwLjA1ODEsMC4zMTQ5OTEgMC4wODIwMywwLjQ3NDYwOSAtMC4wMjM5MSwtMC4xNTkyNjIgLTAuMDUyNTEsLTAuMzE3MjUgLTAuMDgyMDMsLTAuNDc0NjA5IHogbSAtMTMuNjgzNTk0LDAuOTEyMTA5IC0wLjExMTMyOCwwLjA0ODgzIEMgMTQuNzY0OTMyLDE0Ljc1NjA0MyAxNC4zNTkzNzUsMTUuMzMxNjcyIDE0LjM1OTM3NSwxNiBjIDAsMC42NjgzMjkgMC40MDUxMjcsMS4yNDM5NTIgMC45ODQzNzUsMS40OTYwOTQgbCAwLjExMTMyOCwwLjA0ODgzIDAuMDk3NjYsLTAuMjIyNjU2IC0wLjExMTMyOCwtMC4wNDY4NyBDIDE0Ljk0ODAwMywxNy4wNjA2MDYgMTQuNjAxNTYsMTYuNTcwNDM5IDE0LjYwMTU2MywxNiBjIDAsLTAuNTcwNDM4IDAuMzQ2NDQ3LC0xLjA2MDYyIDAuODM5ODQzLC0xLjI3NTM5MSBsIDAuMTExMzI4LC0wLjA0Njg3IHogbSAxMy43NjU2MjUsMy41MjczNDQgYyAtMC4wMjM5MywwLjE1OTYxOCAtMC4wNTI0NiwwLjMxNjkwMSAtMC4wODIwMywwLjQ3NDYwOSAwLjAyOTUyLC0wLjE1NzM1OSAwLjA1ODEzLC0wLjMxNTM0NyAwLjA4MjAzLC0wLjQ3NDYwOSB6IE0gMjkuMTA3NDIyLDE4LjYyNSBjIC0wLjAzMjMsMC4xNjA3MzggLTAuMDY5MzYsMC4zMTk5MyAtMC4xMDc0MjIsMC40Nzg1MTYgMC4wMzgxMywtMC4xNTg3ODcgMC4wNzUwNywtMC4zMTc1NyAwLjEwNzQyMiwtMC40Nzg1MTYgeiBtIC0wLjE0MDYyNSwwLjYyODkwNiBjIC0wLjAyODAyLDAuMTExMDA1IC0wLjA2MSwwLjIyMDIwMyAtMC4wOTE4LDAuMzMwMDc4IDAuMDMwNzcsLTAuMTA5NzE4IDAuMDYzOCwtMC4yMTkyMzIgMC4wOTE4LC0wLjMzMDA3OCB6IG0gLTAuMzkyNTc4LDEuMjc1MzkxIGMgLTAuMDI1MywwLjA2OTU2IC0wLjA1MzY2LDAuMTM4MDE2IC0wLjA4MDA4LDAuMjA3MDMxIDAuMDI2NDksLTAuMDY5MTcgMC4wNTQ3MiwtMC4xMzczMTIgMC4wODAwOCwtMC4yMDcwMzEgeiBtIC0wLjIzODI4MiwwLjYwNzQyMiBjIC0wLjAyNDU5LDAuMDU4NDQgLTAuMDUwNzcsMC4xMTU4MTEgLTAuMDc2MTcsMC4xNzM4MjggMC4wMjUzOSwtMC4wNTc5OCAwLjA1MTU5LC0wLjExNTQyMiAwLjA3NjE3LC0wLjE3MzgyOCB6IG0gLTAuMjY1NjI1LDAuNTg3ODkgYyAtMC4wMywwLjA2MjU2IC0wLjA2MjgsMC4xMjM1MzEgLTAuMDkzNzUsMC4xODU1NDcgMC4wMzA5LC0wLjA2MTg4IDAuMDYzOCwtMC4xMjMxMiAwLjA5Mzc1LC0wLjE4NTU0NyB6IG0gLTAuMjc1MzksMC41NDI5NjkgYyAtMC4wNTEwNCwwLjA5NDk1IC0wLjEwNDkzMiwwLjE4NzY5MSAtMC4xNTgyMDMsMC4yODEyNSAwLjA1MzM2LC0wLjA5MzY4IDAuMTA3MDgxLC0wLjE4NjE2OCAwLjE1ODIwMywtMC4yODEyNSB6IG0gLTAuMjk4ODI4LDAuNTI3MzQ0IGMgLTAuMDY1OTQsMC4xMTAyNjEgLTAuMTM2MDUxLDAuMjE4MDAxIC0wLjIwNTA3OCwwLjMyNjE3MiAwLjA2ODk4LC0wLjEwODA2NCAwLjEzOTE3NiwtMC4yMTYwMjEgMC4yMDUwNzgsLTAuMzI2MTcyIHogbSAtMC4zMzIwMzIsMC41MjczNDQgYyAtMC4wODE5OSwwLjEyMzU2MSAtMC4xNjc5MzEsMC4yNDQ2MDUgLTAuMjUzOTA2LDAuMzY1MjM0IDAuMDg1ODgsLTAuMTIwNDY2IDAuMTcyMDA1LC0wLjI0MTg0NCAwLjI1MzkwNiwtMC4zNjUyMzQgeiBtIC0wLjM0NzY1NiwwLjQ5NjA5MyBjIC0wLjA5NzE4LDAuMTMyOTI5IC0wLjE5Njg4MSwwLjI2MzQzOCAtMC4yOTg4MjgsMC4zOTI1NzggMC4xMDIxMDEsLTAuMTI5Mjg5IDAuMjAxNTA3LC0wLjI1OTQ5IDAuMjk4ODI4LC0wLjM5MjU3OCB6IG0gLTAuMzc1LDAuNDg4MjgyIGMgLTAuMDkwODEsMC4xMTI4OTIgLTAuMTgyOTU0LDAuMjI0MTQ4IC0wLjI3NzM0NCwwLjMzMzk4NCAwLjA5NDI2LC0wLjEwOTY1OSAwLjE4NjY1MSwtMC4yMjEyNzkgMC4yNzczNDQsLTAuMzMzOTg0IHogbSAtMC40Mzk0NTMsMC41MTk1MzEgYyAtMC4wOTA3MiwwLjEwMTcxMiAtMC4xODM1NzUsMC4yMDE4OTUgLTAuMjc3MzQ0LDAuMzAwNzgxIDAuMDkzODcsLTAuMDk4OTYgMC4xODY1MzMsLTAuMTk4OTg4IDAuMjc3MzQ0LC0wLjMwMDc4MSB6IG0gLTAuNDAyMzQ0LDAuNDMxNjQgYyAtMC4xMDE0NjIsMC4xMDQxNTMgLTAuMjA1NzMyLDAuMjA1ODI3IC0wLjMxMDU0NywwLjMwNjY0MSAwLjEwNDk5MiwtMC4xMDA5MzYgMC4yMDg5MTcsLTAuMjAyMzU1IDAuMzEwNTQ3LC0wLjMwNjY0MSB6IG0gLTAuNDcyNjU2LDAuNDYwOTM4IGMgLTAuMDkyNTQsMC4wODYwOCAtMC4xODYyNzksMC4xNzA0NDQgLTAuMjgxMjUsMC4yNTM5MDYgMC4wOTQ3MiwtMC4wODMyMSAwLjE4ODk1NSwtMC4xNjgwOTMgMC4yODEyNSwtMC4yNTM5MDYgeiBtIC0wLjQ3MjY1NiwwLjQxNzk2OSBjIC0wLjA5ODUyLDAuMDgzMzUgLTAuMTk3ODcyLDAuMTY1NTY2IC0wLjI5ODgyOCwwLjI0NjA5MyAwLjEwMTEzNiwtMC4wODA2NCAwLjIwMDEzMiwtMC4xNjI2MTkgMC4yOTg4MjgsLTAuMjQ2MDkzIHogbSAtMC40OTQxNDEsMC4zOTg0MzcgYyAtMC4xMDEwOCwwLjA3NzYyIC0wLjIwMzI5MiwwLjE1Mzc0NSAtMC4zMDY2NCwwLjIyODUxNiAwLjEwMzMzOCwtMC4wNzQ3MyAwLjIwNTU2OSwtMC4xNTA5NCAwLjMwNjY0LC0wLjIyODUxNiB6IG0gLTAuNDg4MjgxLDAuMzU3NDIyIGMgLTAuMTIwMzMxLDAuMDg0MDQgLTAuMjQyMDMzLDAuMTY1OTQzIC0wLjM2NTIzNCwwLjI0NjA5NCAwLjEyMzIxOSwtMC4wODAxMyAwLjI0NDg4NSwtMC4xNjIwNzQgMC4zNjUyMzQsLTAuMjQ2MDk0IHogbSAtMC41MDM5MDYsMC4zMzU5MzggYyAtMC4xMzUyNDQsMC4wODU2MyAtMC4yNzE2NjQsMC4xNjkxMzMgLTAuNDEwMTU3LDAuMjUgMC4xMzgzMiwtMC4wODA3NCAwLjI3NTA3OCwtMC4xNjQ1MTQgMC40MTAxNTcsLTAuMjUgeiBtIC0wLjUxNzU3OCwwLjMxMDU0NiBjIC0wLjE0ODUwNCwwLjA4NDg4IC0wLjI5NzE5MywwLjE2ODc0NyAtMC40NDkyMTksMC4yNDgwNDcgMC4xNTE3MjIsLTAuMDc5MTMgMC4zMDEwMDQsLTAuMTYzMzU1IDAuNDQ5MjE5LC0wLjI0ODA0NyB6IG0gLTAuNTQ0OTIyLDAuMjk2ODc1IGMgLTAuMTYwOTE4LDAuMDgyMyAtMC4zMjM2NDcsMC4xNjIzNyAtMC40ODgyODIsMC4yMzgyODIgMC4xNjUyNjEsLTAuMDc2MTYgMC4zMjY3NjUsLTAuMTU1Njg4IDAuNDg4MjgyLC0wLjIzODI4MiB6IG0gLTAuNTQ2ODc1LDAuMjYzNjcyIGMgLTAuMTcyMzMxLDAuMDc4NDYgLTAuMzQ3MjYsMC4xNTMzMTIgLTAuNTIzNDM4LDAuMjI0NjEgMC4xNzYwNTgsLTAuMDcxMjIgMC4zNTEyMiwtMC4xNDYyMzkgMC41MjM0MzgsLTAuMjI0NjEgeiBtIC0wLjU4Nzg5MSwwLjI1IGMgLTAuMTY0NTEsMC4wNjU1OCAtMC4zMzAzNzUsMC4xMjgzMDkgLTAuNDk4MDQ3LDAuMTg3NSAwLjE2NzcyMywtMC4wNTkxOCAwLjMzMzQ4NiwtMC4xMjE5MjUgMC40OTgwNDcsLTAuMTg3NSB6IG0gLTAuNjg1NTQ3LDAuMjUxOTUzIGMgLTAuMTQyMzAzLDAuMDQ3ODcgLTAuMjg1Mjc2LDAuMDkzNTMgLTAuNDI5Njg3LDAuMTM2NzE5IDAuMTQ0NDU2LC0wLjA0MzE4IDAuMjg3MzM5LC0wLjA4ODg2IDAuNDI5Njg3LC0wLjEzNjcxOSB6IG0gLTAuNTY2NDA2LDAuMTc1NzgyIGMgLTAuMTczMjUyLDAuMDQ5OCAtMC4zNDczNjEsMC4wOTc2OCAtMC41MjM0MzgsMC4xNDA2MjUgLTAuMjA3NDIsMC4wNTA1OSAtMC40MTU4NDYsMC4wOTc4MyAtMC42MjY5NTMsMC4xMzg2NzIgMC4yMTExMTMsLTAuMDQwODIgMC40MTk1MjUsLTAuMDg4MSAwLjYyNjk1MywtMC4xMzg2NzIgMC4xNzU5MTMsLTAuMDQyODkgMC4zNTAzNDMsLTAuMDkwODkgMC41MjM0MzgsLTAuMTQwNjI1IHogbSAtMS4xNjc5NjksMC4yODEyNSBjIC0wLjIwODA4MywwLjAzOTk3IC0wLjQxNzQ5NCwwLjA3NTIxIC0wLjYyODkwNiwwLjEwNTQ2OCAtMC4yMTE4ODUsMC4wMzAzMyAtMC40MjU2MzksMC4wNTU4NCAtMC42NDA2MjUsMC4wNzYxNyAwLjIxNTA2LC0wLjAyMDMzIDAuNDI4NjY1LC0wLjA0NTg0IDAuNjQwNjI1LC0wLjA3NjE3IDAuMjExMjg0LC0wLjAzMDIzIDAuNDIwOTQ1LC0wLjA2NTU0IDAuNjI4OTA2LC0wLjEwNTQ2OCB6IiAgICBzdHlsZT0iYmFzZWxpbmUtc2hpZnQ6YmFzZWxpbmU7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2ZWN0b3ItZWZmZWN0Om5vbmU7ZmlsbDojMjIyMjIyO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO3N0b3AtY29sb3I6IzAwMDAwMDtzdG9wLW9wYWNpdHk6MSIgICAgaWQ9InBhdGgxNCIgLz48L3N2Zz4=');}.icon-jakevan{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MiIgdmlld0JveD0iMCAwIDMyIDMyIj48cGF0aCBkPSJNMTcuODggMTQuNjhIMTIuOWwtLjQzLTEuNjNIOS41OGwtLjQ1IDEuNjNINi41bDIuODktOC43NGgzLjJsMi44OSA4LjY0di04LjZoMi40djMuNzhjLjEtLjIuMjItLjM4LjM1LS41Ny4xMy0uMi4yNi0uMzcuMzktLjU0bDEuODYtMi42N2g3Ljh2MS44OUgyNS40djEuMzdoMi42NXYxLjg4SDI1LjR2MS42NWgyLjg2djEuOTFoLTcuOTNsLTEuNzUtMy4zMi0uNy40MXptNS4xMy04LjU5LTIuNyAzLjc5IDIuNyA0Ljc0em0tMTEuMDUgNS4wMy0uMzgtMS40M2ExMzYuODYgMTM2Ljg2IDAgMCAwLS40LTEuNTVMMTEgNy4zOGExNy43NiAxNy43NiAwIDAgMS0uMzYgMS42bC0uMTguNzEtLjM5IDEuNDN6bS04LjU4IDYuM2E1Ljc0IDUuNzQgMCAwIDEtMS4yNC0uMTN2LTEuODNsLjQxLjA4Yy4xNS4wMy4zLjA1LjQ3LjA1LjMgMCAuNTEtLjA2LjY3LS4xN2EuOTIuOTIgMCAwIDAgLjM0LS41MmMuMDYtLjIzLjEtLjUyLjEtLjg2VjUuOThoMi40djcuODVjMCAuODgtLjEzIDEuNTctLjQgMi4xLS4yNi41Mi0uNjMuOS0xLjEgMS4xNC0uNDguMjMtMS4wMy4zNS0xLjY1LjM1WiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE4NDM5MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0yMi44MiAyMi4yN2gtNC4wNmwtLjM3LTEuNEgxNS45bC0uMzkgMS40aC0yLjI2bDIuNDktNy41M2gyLjc1bDIuNDkgNy40NHYtNy40MWgyLjdsMi43NyA1LjIxaC4wM2E0MS4xIDQxLjEgMCAwIDEtLjA3LTEuODJ2LTMuMzloMS44M3Y3LjVoLTIuN2wtMi43OS01LjI4aC0uMDRhMTIuODMgMTIuODMgMCAwIDEgLjA4IDEuMjZsLjAyLjY0em0tNC44Ni0zLjA3LS4zMy0xLjIzYTg5LjA3IDg5LjA3IDAgMCAwLS4zNS0xLjM0bC0uMTQtLjY1YTE1LjA0IDE1LjA0IDAgMCAxLS4zMSAxLjM3bC0uMTYuNjItLjMzIDEuMjN6bS0zLjg1LTQuNDMtMi41IDcuNUg5LjJsLTIuNS03LjVoMi4zMmwxLjA0IDMuOGExNS4wMyAxNS4wMyAwIDAgMSAuMzYgMS43NiA3LjYxIDcuNjEgMCAwIDEgLjItMS4ybC4xNC0uNTQgMS4wNi0zLjgyeiIgc3R5bGU9ImZpbGw6Y3VycmVudENvbG9yO3N0cm9rZS13aWR0aDouMDE1OTg4NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40IC42Nikgc2NhbGUoLjk2MDUwMTM0KSIvPjxwYXRoIGQ9Ik0xMS45IDI0LjIxYzAgLjQtLjA3LjcyLS4yLjk5LS4xNS4yNi0uMzYuNDYtLjYzLjYtLjI4LjEzLS42Mi4yLTEuMDMuMkg5LjJ2LTMuNWguOTdjLjM4IDAgLjcuMDYuOTYuMTkuMjUuMTMuNDUuMzIuNTguNTguMTQuMjUuMi41Ny4yLjk0em0tLjI2LjAxYzAtLjMzLS4wNS0uNjEtLjE2LS44M2ExLjEgMS4xIDAgMCAwLS41MS0uNTEgMS45NSAxLjk1IDAgMCAwLS44Ny0uMTdoLS42NnYzLjA3aC42Yy41MyAwIC45My0uMTMgMS4yLS4zOS4yNy0uMjYuNC0uNjUuNC0xLjE3ek0xNC4yNyAyNmgtMS45NXYtMy41aDEuOTV2LjIyaC0xLjd2MS4zMmgxLjZ2LjIzaC0xLjZ2MS41aDEuN3ptMS4yOC0zLjVjLjI4IDAgLjUyLjAyLjcuMDhhLjguOCAwIDAgMSAuNDQuM2MuMS4xNC4xNC4zMy4xNC41N2EuOS45IDAgMCAxLS4xLjQ1Ljg3Ljg3IDAgMCAxLS4yNy4zMmMtLjEyLjA4LS4yNS4xNC0uNC4xOGwuOTggMS42aC0uM2wtLjkyLTEuNTNoLS44OVYyNmgtLjI1di0zLjV6bS0uMDMuMjFoLS41OXYxLjU1aC43MWMuMyAwIC41Mi0uMDcuNjktLjIxLjE2LS4xNC4yNC0uMzQuMjQtLjYgMC0uMjgtLjA5LS40Ny0uMjYtLjU4LS4xNy0uMS0uNDMtLjE2LS43OS0uMTZ6bTUuNTctLjIyTDIwLjEyIDI2aC0uMjVsLS43Ni0yLjY1LS4wNS0uMTYtLjA0LS4xNGExOC44IDE4LjggMCAwIDEtLjA2LS4yNCAyMC42IDIwLjYgMCAwIDEtLjExLjQ4TDE4LjA5IDI2aC0uMjVsLS45Ni0zLjVoLjI2bC42NyAyLjQ3YTI3LjM2IDI3LjM2IDAgMCAxIC4wOS4zNWwuMDQuMTcuMDMuMTUuMDMtLjE2YTQuODMgNC44MyAwIDAgMSAuMTQtLjUzbC43LTIuNDZoLjI1bC43MyAyLjQ4YTExLjk4IDExLjk4IDAgMCAxIC4xMy41M2wuMDQuMTVhMTEuMDIgMTEuMDIgMCAwIDEgLjE1LS42OGwuNjktMi40OHpNMjMuMjYgMjZoLTEuOTV2LTMuNWgxLjk1di4yMmgtMS43djEuMzJoMS42di4yM2gtMS42djEuNWgxLjd6bTEuMjgtMy41Yy4yOCAwIC41Mi4wMi43MS4wOGEuOC44IDAgMCAxIC40My4zYy4xLjE0LjE0LjMzLjE0LjU3YS45LjkgMCAwIDEtLjEuNDUuODcuODcgMCAwIDEtLjI3LjMyYy0uMTEuMDgtLjI1LjE0LS40LjE4bC45OCAxLjZoLS4zbC0uOTItMS41M2gtLjg4VjI2aC0uMjZ2LTMuNXptLS4wMi4yMWgtLjZ2MS41NWguNzJjLjI5IDAgLjUxLS4wNy42OC0uMjEuMTYtLjE0LjI0LS4zNC4yNC0uNiAwLS4yOC0uMDgtLjQ3LS4yNi0uNTgtLjE3LS4xLS40My0uMTYtLjc4LS4xNnpNMjYuNSAyNmgtLjI1di0zLjVoMS45NXYuMjJoLTEuN3YxLjQ5aDEuNnYuMjJoLTEuNnoiIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvcjtzdHJva2Utd2lkdGg6LjAxMDEwNjgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNCAuNjYpIHNjYWxlKC45NjA1MDEzNCkiLz48L3N2Zz4=');}.icon-user-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRaTTk0LDEyMGEzNCwzNCwwLDEsMSwzNCwzNEEzNCwzNCwwLDAsMSw5NCwxMjBaTTY1Ljc3LDIxMGE2Ni40Myw2Ni40MywwLDAsMSwyMC43Ny0yOS4zNiw2Niw2NiwwLDAsMSw4Mi45MiwwQTY2LjQzLDY2LjQzLDAsMCwxLDE5MC4yMywyMTBaTTIxMCwyMDhhMiwyLDAsMCwxLTIsMmgtNS4xN2E3Ny44NSw3Ny44NSwwLDAsMC00OS4zOC01MS43MSw0Niw0NiwwLDEsMC01MC45LDBBNzcuODUsNzcuODUsMCwwLDAsNTMuMTcsMjEwSDQ4YTIsMiwwLDAsMS0yLTJWNDhhMiwyLDAsMCwxLDItMkgyMDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-chat-teardrop{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzIsMjZhOTguMTEsOTguMTEsMCwwLDAtOTgsOTh2ODRhMTQsMTQsMCwwLDAsMTQsMTRoODRhOTgsOTgsMCwwLDAsMC0xOTZabTAsMTg0SDQ4YTIsMiwwLDAsMS0yLTJWMTI0YTg2LDg2LDAsMSwxLDg2LDg2WiIvPjwvc3ZnPg==');}.icon-house-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNkgyMTZhNiw2LDAsMCwwLDYtNlYxMjBBMTMuOTIsMTMuOTIsMCwwLDAsMjE3LjksMTEwLjFaTTIxMCwyMTBINDZWMTIwYTIsMiwwLDAsMSwuNTgtMS40Mmw4MC04MGEyLDIsMCwwLDEsMi44NCwwbDgwLDgwQTIsMiwwLDAsMSwyMTAsMTIwWiIvPjwvc3ZnPg==');}.icon-caret-left{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMjAzLjc2YTYsNiwwLDEsMS04LjQ4LDguNDhsLTgwLTgwYTYsNiwwLDAsMSwwLTguNDhsODAtODBhNiw2LDAsMCwxLDguNDgsOC40OEw4OC40OSwxMjhaIi8+PC9zdmc+');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-caret-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDE2Ny41MSwxMjgsOTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTgwLjI0LDEzMi4yNFoiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-shuffle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTc5Ljc2YTYsNiwwLDAsMSwwLDguNDhsLTI0LDI0YTYsNiwwLDAsMS04LjQ4LTguNDhMMjE3LjUyLDE5MEgyMDAuOTRhNzAuMTYsNzAuMTYsMCwwLDEtNTctMjkuMzFsLTQxLjcxLTU4LjRBNTguMTEsNTguMTEsMCwwLDAsNTUuMDYsNzhIMzJhNiw2LDAsMCwxLDAtMTJINTUuMDZhNzAuMTYsNzAuMTYsMCwwLDEsNTcsMjkuMzFsNDEuNzEsNTguNEE1OC4xMSw1OC4xMSwwLDAsMCwyMDAuOTQsMTc4aDE2LjU4bC0xMy43Ni0xMy43NmE2LDYsMCwwLDEsOC40OC04LjQ4Wm0tOTIuMDYtNzQuNDFhNS45MSw1LjkxLDAsMCwwLDMuNDgsMS4xMiw2LDYsMCwwLDAsNC44OS0yLjUxbDEuMTktMS42N0E1OC4xMSw1OC4xMSwwLDAsMSwyMDAuOTQsNzhoMTYuNThMMjAzLjc2LDkxLjc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMjQtMjRhNiw2LDAsMCwwLDAtOC40OGwtMjQtMjRhNiw2LDAsMCwwLTguNDgsOC40OEwyMTcuNTIsNjZIMjAwLjk0YTcwLjE2LDcwLjE2LDAsMCwwLTU3LDI5LjMxTDE0Mi43OCw5N0E2LDYsMCwwLDAsMTQ0LjE4LDEwNS4zNVptLTMyLjM2LDQ1LjNhNiw2LDAsMCwwLTguMzcsMS4zOWwtMS4xOSwxLjY3QTU4LjExLDU4LjExLDAsMCwxLDU1LjA2LDE3OEgzMmE2LDYsMCwwLDAsMCwxMkg1NS4wNmE3MC4xNiw3MC4xNiwwLDAsMCw1Ny0yOS4zMWwxLjE5LTEuNjdBNiw2LDAsMCwwLDExMS44MiwxNTAuNjVaIi8+PC9zdmc+');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsNzJhNiw2LDAsMCwxLTYsNkg5NFYyMDEuNTFsMzcuNzYtMzcuNzVhNiw2LDAsMCwxLDguNDgsOC40OGwtNDgsNDhhNiw2LDAsMCwxLTguNDgsMGwtNDgtNDhhNiw2LDAsMCwxLDguNDgtOC40OEw4MiwyMDEuNTFWNzJhNiw2LDAsMCwxLDYtNkgyMzJBNiw2LDAsMCwxLDIzOCw3MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTY0LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OCwwbC00OC00OGE2LDYsMCwxLDEsOC40OC04LjQ4TDE3MCwxOTMuNTFWNzBIMzJhNiw2LDAsMCwxLDAtMTJIMTc2YTYsNiwwLDAsMSw2LDZWMTkzLjUxbDM3Ljc2LTM3Ljc1YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+');}.icon-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+');}.icon-dots-three{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTI4YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxMjhaTTYwLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw2MCwxMThabTEzNiwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDE5NiwxMThaIi8+PC9zdmc+');}.icon-star-half-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMCwyNC41LDE3LjgyTDEyOCwxOTguNDlsNTAuNTMsMzEuMDhBMTYuNCwxNi40LDAsMCwwLDIwMywyMTEuNzVsLTEzLjc2LTU4LjA3LDQ1LTM4LjgzQTE2LjQzLDE2LjQzLDAsMCwwLDIzOS4xOCw5Ny4yNlptLTE1LjM0LDUuNDctNDguNyw0MmE4LDgsMCwwLDAtMi41Niw3LjkxbDE0Ljg4LDYyLjhhLjM3LjM3LDAsMCwxLS4xNy40OGMtLjE4LjE0LS4yMy4xMS0uMzgsMGwtNTQuNzItMzMuNjVBOCw4LDAsMCwwLDEyOCwxODEuMVYzMmMuMjQsMCwuMjcuMDguMzUuMjZMMTUzLDkxLjg2YTgsOCwwLDAsMCw2Ljc1LDQuOTJsNjMuOTEsNS4xNmMuMTYsMCwuMjUsMCwuMzQuMjlTMjI0LDEwMi42MywyMjMuODQsMTAyLjczWiIvPjwvc3ZnPg==');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');}.icon-heart-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==');}
\ No newline at end of file
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index e9f2124..a11598a 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn_)}.on-this-page ul{width:100%}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.letters li{flex:1;max-width:calc(7.69% - 2px)}@media (min-width:768px){nav.letters li{flex:0 1 auto;max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
+nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}.on-this-page ul{width:100%}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:flex-start;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:max-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--trans-base)}.index li.active,.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}
\ No newline at end of file
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index f6eee56..2471e91 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -91,7 +91,7 @@
});
this.queue.subscribe((event, data) => {
- if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
+ if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
if (event === 'operation-completed') {
this.handleQueueSuccess(event, data);
} else if (event === 'operation-failed-permanent') {
@@ -207,7 +207,6 @@
}
async handleQueueSuccess(event, data) {
this.store.clearCache();
- this.store.clearHttpHeaders();
this.store.fetch();
}
handleQueueFailure(event, data) {
@@ -234,6 +233,20 @@
uploader: 'details.uploader'
};
this.ui = window.uiFromSelectors(this.elements);
+ if (this.ui.uploader) {
+ window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
+
+ window.jvbUploads.subscribe((event, data) => {
+ if (event === 'sent-to-queue') {
+ console.log(data);
+ if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
+ window.debouncer.schedule('crud-complete', ()=> {
+ this.store.clearHttpHeaders();
+ });
+ }
+ }
+ });
+ }
this.isTimeline = !!document.querySelector('[data-timeline]');
}
init() {
@@ -316,7 +329,7 @@
break;
case 'create':
- this.modals.create.dataset.itemID = 'new';
+ this.modals.create.dataset.itemId = 'new';
this.modals.create.dataset.content = this.content;
this.modals.create.handleOpen();
break;
@@ -646,7 +659,7 @@
let item = this.store.get(parseInt(itemID));
if (item) {
- this.ui.modals.edit.dataset.itemID = itemID;
+ this.ui.modals.edit.dataset.itemId = itemID;
this.ui.modals.edit.dataset.content = this.content;
let form = this.ui.modals.edit.querySelector('form');
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index d4cb8bc..95027e3 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -6,6 +6,7 @@
* this.store = window.jvbStore.register('feed', { config });
*/
class DataStore {
+
constructor() {
// Singleton pattern
if (DataStore.instance) {
@@ -140,6 +141,8 @@
delete: (id) => this.delete(name, id),
get: (id) => this.get(name, id),
getAll: () => this.getAll(name),
+ getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
+ filterByIndex: (criteria) => this.filterByIndex(name, criteria),
getFiltered: () => this.getFiltered(name),
clear: () => this.clear(name),
@@ -747,30 +750,33 @@
// Reject functions
if (type === 'function') {
- return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+ if (validate) return { valid: false, error: `Function at ${path}` };
+ console.debug(`[DataStore] Stripped function at ${path}`);
+ return { valid: true, data: undefined };
}
// DOM elements
if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
- return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+ if (validate) return { valid: false, error: `DOM element at ${path}` };
+ console.debug(`[DataStore] Stripped DOM element at ${path}`);
+ return { valid: true, data: undefined };
}
// FormData - convert and continue
if (obj instanceof FormData) {
- return validate
- ? { valid: false, error: `FormData at ${path}` }
- : { valid: true, data: this.formDataToObject(obj) };
+ if (validate) return { valid: false, error: `FormData at ${path}` };
+ console.debug(`[DataStore] Converted FormData at ${path}`);
+ return { valid: true, data: this.formDataToObject(obj) };
}
// Preserve safe types
- if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+ if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj) || obj instanceof Blob) {
return { valid: true, data: obj };
}
// Convert Sets to Arrays
if (obj instanceof Set) {
- const arr = Array.from(obj);
- return this.processForStorage(arr, validate, path);
+ return this.processForStorage(Array.from(obj), validate, path);
}
// Convert Maps to Objects
@@ -784,7 +790,7 @@
for (let i = 0; i < obj.length; i++) {
const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
if (!result.valid) return result;
- if (result.data !== null) processed.push(result.data);
+ if (result.data !== undefined) processed.push(result.data);
}
return { valid: true, data: processed };
}
@@ -795,14 +801,14 @@
for (const [key, value] of Object.entries(obj)) {
const result = this.processForStorage(value, validate, `${path}.${key}`);
if (!result.valid) return result;
- if (result.data !== null) processed[key] = result.data;
+ if (result.data !== undefined) processed[key] = result.data;
}
return { valid: true, data: processed };
}
- return validate
- ? { valid: false, error: `Unknown type at ${path}` }
- : { valid: true, data: null };
+ if (validate) return { valid: false, error: `Unknown type at ${path}` };
+ console.debug(`[DataStore] Stripped unknown type at ${path}`);
+ return { valid: true, data: undefined };
}
/***********************************************************************
@@ -829,6 +835,64 @@
const store = this.stores.get(name);
return Array.from(store.data.values());
}
+ /**
+ * Filter in-memory data by multiple index/value pairs
+ * @param {string} name - Store name
+ * @param {Object} criteria - Object of { indexName: acceptedValue(s) }
+ * @returns {Array} - Items matching ALL criteria
+ *
+ * @example
+ * filterByIndex(name, { field: 'upload_123', status: ['queued', 'uploading'] })
+ */
+ filterByIndex(name, criteria) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ return Array.from(store.data.values()).filter(item => {
+ return Object.entries(criteria).every(([key, value]) => {
+ const accepted = Array.isArray(value) ? value : [value];
+ return accepted.includes(item[key]);
+ });
+ });
+ }
+ /**
+ * Get all items matching an index value
+ * @param {string} name - Store name
+ * @param {string} indexName - Name of the index to query
+ * @param {*} value - Value to match
+ * @returns {Promise<Array>} - Matching items
+ */
+ async getAllByIndex(name, indexName, value) {
+ const store = this.stores.get(name);
+ const values = Array.isArray(value) ? value : [value];
+
+ // Try IndexedDB index query first (more efficient for large datasets)
+ if (store.db && store.db.objectStoreNames.contains(store.config.storeName)) {
+ try {
+ const tx = store.db.transaction([store.config.storeName], 'readonly');
+ const objectStore = tx.objectStore(store.config.storeName);
+
+ if (objectStore.indexNames.contains(indexName)) {
+ const index = objectStore.index(indexName);
+
+ const results = await Promise.all(
+ values.map(v => new Promise((resolve, reject) => {
+ const request = index.getAll(v);
+ request.onsuccess = () => resolve(request.result || []);
+ request.onerror = () => reject(request.error);
+ }))
+ );
+
+ return results.flat();
+ }
+ } catch (error) {
+ console.warn(`Index query failed for "${indexName}", falling back to filter:`, error);
+ }
+ }
+
+ // Fallback: filter in-memory data
+ return Array.from(store.data.values()).filter(item => values.includes(item[indexName]));
+ }
getFiltered(name) {
const store = this.stores.get(name);
@@ -859,12 +923,8 @@
}
/***********************************************************************
- * FILTER OPERATIONS (UNIFIED)
+ * FILTER OPERATIONS
***********************************************************************/
-
- /**
- * Unified filter update - handles all filter operations
- */
async updateFilters(name, updates, clearAll = false) {
const store = this.stores.get(name);
const oldFilters = { ...store.filters };
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 8e5fbdc..a0388e2 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -1,7 +1,3 @@
-/**
- * Enhanced FormController - Manages forms with special fields, caching, and queue integration
- * Works with DataStore for CRUD operations and standalone for front-end forms
- */
class FormController {
constructor(config = {}) {
this.config = {
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 16ad1bf..7981602 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -1,391 +1,302 @@
-/**
- * HandleSelection - Reusable selection management for items in grids
- *
- * Handles selection logic including:
- * - Individual item selection/deselection
- * - Select all / Clear all
- * - Range selection (shift+click)
- * - Selection count updates
- * - Bulk action visibility
- *
- * @class HandleSelection
- */
class HandleSelection {
- /**
- * @param {Object} config - Configuration object
- * @param {HTMLElement} config.container - Container element holding items
- * @param {Object} config.ui - UI element references
- * @param {HTMLElement} config.ui.selectAll - Select all checkbox
- * @param {HTMLElement} config.ui.bulkControls - Bulk actions container
- * @param {HTMLElement} config.ui.count - Selection count display
- * @param {string} config.itemSelector - Selector for individual items
- * @param {string} config.checkboxSelector - Selector for item checkboxes
- * @param {Function} config.onSelectionChange - Optional callback when selection changes
- */
- constructor(config) {
- this.container = config.container;
- this.ui = config.ui || {};
- this.itemSelector = config.itemSelector || '.item';
- this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]';
+ constructor(options) {
+ this.container = options.container; // An actual element, not class name
+ this.selectors = {
+ item: options.item || '.item',
+ count: options.count || '.selection-count',
+ bulkControls: options.bulkControls || '.selection-actions',
+ checkbox: options.checkbox || '[name*="select-item"]',
+ selectAll: options.selectAll || '[data-select-all]',
+ wrapper: options.wrapper || ':has(.item-grid)'
+ };
+
+ this.ui = window.uiFromSelectors(this.selectors, this.container)
this.selectedItems = new Set();
- this.lastSelected = null;
+ this.lastSelected = null; // For shift+click range selection
+ this.lastSelectedWrapper = null; //Tracks which wrapper we're in
+ this.lastClicked = null;
this.subscribers = new Set();
- this.init();
+ this.initListeners();
}
- init() {
- // Bind event handlers
+ initListeners() {
this.clickHandler = this.handleClick.bind(this);
this.changeHandler = this.handleChange.bind(this);
this.keyHandler = this.handleKeys.bind(this);
- // Attach listeners
- this.container.addEventListener('click', this.clickHandler);
this.container.addEventListener('change', this.changeHandler);
+ this.container.addEventListener('click', this.clickHandler);
this.container.addEventListener('keydown', this.keyHandler);
}
+ handleChange(e) {
+ // Select all
+ if (e.target.matches(this.selectors.selectAll)) {
+ this.handleSelectAll(e.target);
+ return;
+ }
+
+ // Individual checkbox
+ if (e.target.matches(this.selectors.checkbox)) {
+ const item = e.target.closest(this.selectors.item);
+ if (!item) return;
+
+ // Find the immediate wrapper - check group first, then preview
+ const wrapper = this.getItemWrapper(item);
+ const id = this.getItemId(item);
+
+ // Clear selection if clicking in different wrapper without shift
+ if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
+ this.clearSelection();
+ }
+
+ if (e.target.checked) {
+ this.select(id, false);
+ } else {
+ this.deselect(id, false);
+ }
+
+ this.lastSelected = id;
+ this.lastSelectedWrapper = wrapper;
+ }
+ }
+
+ handleClick(e) {
+ const item = e.target.closest(this.selectors.item);
+ const wrapper = item ? this.getItemWrapper(item) : null;
+
+ if (wrapper) {
+ this.lastClicked = wrapper;
+ }
+
+ // Handle non-checkbox clicks on items
+ if (item && !e.target.matches(this.selectors.checkbox)) {
+ if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
+ this.clearSelection();
+ this.lastSelectedWrapper = wrapper;
+ }
+ }
+
+ // Shift+click for range selection
+ if (!e.shiftKey) return;
+
+ const checkbox = e.target.closest(this.selectors.checkbox);
+ if (!checkbox || !this.lastSelected || !this.lastSelectedWrapper) return;
+
+ if (!item || !wrapper) return;
+
+ // Range selection only works within the same wrapper
+ if (wrapper !== this.lastSelectedWrapper) return;
+
+ const items = Array.from(wrapper.querySelectorAll(this.selectors.item));
+ const currentId = this.getItemId(item);
+
+ const lastIndex = items.findIndex(el => this.getItemId(el) === this.lastSelected);
+ const currentIndex = items.findIndex(el => this.getItemId(el) === currentId);
+
+ if (lastIndex === -1 || currentIndex === -1) return;
+
+ const [start, end] = [Math.min(lastIndex, currentIndex), Math.max(lastIndex, currentIndex)];
+ const rangeItems = items.slice(start, end + 1);
+
+ rangeItems.forEach(rangeItem => {
+ this.select(this.getItemId(rangeItem));
+ });
+
+ this.notify('range-selected', {
+ selectedItems: new Set(this.selectedItems),
+ wrapper: wrapper
+ });
+ }
+
+ getItemWrapper(item) {
+ if (!item) return null;
+
+ // Split the compound selector and check each one
+ const wrapperSelectors = this.selectors.wrapper.split(',').map(s => s.trim());
+
+ for (const selector of wrapperSelectors) {
+ const wrapper = item.closest(selector);
+ if (wrapper) return wrapper;
+ }
+
+ return null;
+ }
+
handleKeys(e) {
// Ctrl/Cmd + A: Select all
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault();
- if (this.ui.selectAll) {
- this.ui.selectAll.checked = true;
- this.selectAll(true);
- if (window.jvbA11y) {
- window.jvbA11y.announce('All items selected');
+ if (this.lastClicked) {
+ let check = this.lastClicked.querySelector(this.selectors.selectAll);
+ if (check) {
+ check.checked = true;
}
}
}
// Escape: Deselect all
if (e.key === 'Escape' && this.selectedItems.size > 0) {
- this.selectAll(false);
+ this.clearSelection();
if (window.jvbA11y) {
window.jvbA11y.announce('Selection cleared');
}
}
+ }
- // Delete/Backspace: Remove selected items
- if ((e.key === 'Delete' || e.key === 'Backspace') &&
- !e.target.matches('input, textarea')
- && this.selectedItems > 0) {
- e.preventDefault();
- if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) {
- this.deselect(this.selectedItems);
+ handleSelectAll(trigger) {
+ const wrapper = this.getItemWrapper(trigger) || trigger.closest(this.selectors.wrapper);
+ if (!wrapper) return;
+
+ // Clear any existing selection from other wrappers first
+ if (this.lastSelectedWrapper && wrapper !== this.lastSelectedWrapper) {
+ this.clearSelection();
+ }
+
+ const items = wrapper.querySelectorAll(this.selectors.item);
+ const ids = Array.from(items).map(item => this.getItemId(item));
+
+ if (trigger.checked) {
+ ids.forEach(id => this.select(id, true, false));
+ this.lastSelectedWrapper = wrapper;
+ } else {
+ ids.forEach(id => this.deselect(id, true, false));
+ if (this.selectedItems.size === 0) {
+ this.lastSelectedWrapper = null;
}
}
- }
- handleClick(e) {
- const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`);
- if (!checkbox) return;
-
- // Get the actual checkbox element
- const input = checkbox.tagName === 'LABEL'
- ? document.getElementById(checkbox.getAttribute('for'))
- : checkbox;
-
- if (!input) return;
-
- // Handle shift+click for range selection
- if (e.shiftKey && this.lastSelected) {
- e.preventDefault();
- this.handleRangeSelection(input);
- } else {
- // Store last clicked for range selection
- const item = input.closest(this.itemSelector);
- if (item) {
- this.lastSelected = item;
- }
+ let label = trigger.nextElementSibling || trigger.previousElementSibling;
+ if (label && label.tagName === 'LABEL') {
+ label.textContent = (trigger.checked && items.length > 0) ? 'Clear Selection' : 'Select All';
}
- }
-
- handleChange(e) {
- if (this.ui.selectAll && e.target === this.ui.selectAll) {
- this.selectAll(e.target.checked);
- } else {
- const checkbox = e.target.closest(this.checkboxSelector);
- if (!checkbox) return;
-
- this.toggleSelection(this.getItemId(checkbox));
- }
- }
-
- /**
- * Toggle selection state of an item
- */
- toggleSelection(id) {
- if (!id) return;
-
- let selected = true;
- if (this.selectedItems.has(id)) {
- selected = false;
- this.selectedItems.delete(id);
- } else {
- this.selectedItems.add(id);
- }
-
- if (selected) {
- this.notify('item-selected', {
- selectedItem: id,
- selectedItems: this.selectedItems,
- container: this.container
- });
- } else {
- this.notify('item-deselected', {
- selectedItem: id,
- selectedItems: this.selectedItems,
- container: this.container
- });
- }
-
-
this.updateSelectionUI();
- }
-
- /**
- * Select or deselect all items
- */
- selectAll(checked) {
- const items = this.container.querySelectorAll(this.itemSelector);
-
- if (!checked) {
- this.selectedItems.clear();
- if (this.ui.selectAll) {
- this.ui.selectAll.checked = false;
- }
- }
-
- items.forEach(item => {
- const id = this.getItemId(item);
- const checkbox = item.querySelector(this.checkboxSelector);
-
- if (checkbox) {
- checkbox.checked = checked;
- }
-
- if (checked && id) {
- this.selectedItems.add(id);
- }
- });
this.notify('select-all', {
- container: this.container,
- selected: checked,
- items: items
- })
-
- this.updateSelectionUI();
- }
-
- /**
- * Clear all selections
- */
- clearSelection() {
- this.selectAll(false);
- }
-
- /**
- * Handle shift+click range selection
- */
- handleRangeSelection(currentCheckbox) {
- if (!this.lastSelected) {
- this.lastSelected = currentCheckbox.closest(this.itemSelector);
- return;
- }
-
- const currentItem = currentCheckbox.closest(this.itemSelector);
- if (!currentItem) return;
-
- // Get all items
- const allItems = Array.from(this.container.querySelectorAll(this.itemSelector));
-
- // Find indices
- const lastIndex = allItems.indexOf(this.lastSelected);
- const currentIndex = allItems.indexOf(currentItem);
-
- if (lastIndex === -1 || currentIndex === -1) return;
-
- // Determine range (handle both directions)
- const startIndex = Math.min(lastIndex, currentIndex);
- const endIndex = Math.max(lastIndex, currentIndex);
-
- let isChecked = !currentCheckbox.checked;
- // Select all items in range
- for (let i = startIndex; i <= endIndex; i++) {
- const item = allItems[i];
- const checkbox = item.querySelector(this.checkboxSelector);
- const id = this.getItemId(item);
-
- if (checkbox && id) {
- checkbox.checked = isChecked;
- this.selectedItems.add(id);
- }
- }
-
- // Update last selected to current
- this.lastSelected = currentItem;
-
- this.updateSelectionUI();
-
- this.notify('range-selected', {
- selectedItems: this.selectedItems,
- container: this.container
+ wrapper: wrapper,
+ checked: trigger.checked,
+ ids: ids,
+ selectedItems: new Set(this.selectedItems)
});
-
- // Announce for accessibility
- const selectedCount = endIndex - startIndex + 1;
- if (window.jvbA11y) {
- window.jvbA11y.announce(`Selected ${selectedCount} items in range`);
- }
}
- /**
- * Update selection UI elements
- */
- updateSelectionUI() {
- const count = this.selectedItems.size;
- const totalItems = this.container.querySelectorAll(this.itemSelector).length;
+ getItemId(item) {
+ return item.dataset.uploadId;
+ }
- // Update bulk controls visibility
- if (this.ui.bulkControls) {
- this.ui.bulkControls.hidden = count === 0;
+ /*******************************************************************
+ * PUBLIC API
+ *******************************************************************/
+ select(id, updateCheckbox = true, updateUI = true) {
+ if (this.selectedItems.has(id)) return;
+
+ this.selectedItems.add(id);
+ if (updateCheckbox) this.setCheckboxState(id, true);
+ if (updateUI) {
+ this.updateSelectionUI();
}
+ this.notify('item-selected', { id, selectedItems: new Set(this.selectedItems) });
+ }
- // Update count display
- if (this.ui.count) {
- const itemText = count === 1 ? 'item' : 'items';
- this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
- this.ui.count.hidden = count === 0;
+ deselect(id, updateCheckbox = true, updateUI = true) {
+ if (!this.selectedItems.has(id)) return;
+
+ this.selectedItems.delete(id);
+ if (updateCheckbox) this.setCheckboxState(id, false);
+ if (updateUI) {
+ this.updateSelectionUI();
}
+ this.notify('item-deselected', { id, selectedItems: new Set(this.selectedItems) });
+ }
- // Update select all checkbox state
- if (this.ui.selectAll) {
- this.ui.selectAll.checked = totalItems > 0 && count === totalItems;
- this.ui.selectAll.indeterminate = count > 0 && count < totalItems;
+ toggle(id) {
+ this.selectedItems.has(id) ? this.deselect(id) : this.select(id);
+ this.updateSelectionUI();
+ }
- // Update label text if available
- const label = this.ui.selectAll.nextElementSibling ||
- this.ui.selectAll.previousElementSibling;
- if (label && label.tagName === 'LABEL') {
- label.textContent = (totalItems > 0 && count === totalItems)
- ? 'Clear Selection'
- : 'Select All';
+ clearSelection() {
+ this.selectedItems.forEach(id => this.setCheckboxState(id, false));
+ this.selectedItems.clear();
+ this.lastSelected = null;
+ this.lastSelectedWrapper = null;
+
+ // Uncheck all select-all triggers
+ this.container.querySelectorAll(this.selectors.selectAll).forEach(trigger => {
+ trigger.checked = false;
+ const label = trigger.nextElementSibling || trigger.previousElementSibling;
+ if (label?.tagName === 'LABEL') {
+ label.textContent = 'Select All';
}
- }
+ });
+ this.updateSelectionUI();
+ this.notify('selection-cleared', { selectedItems: new Set() });
}
- /**
- * Get item ID from element
- */
- getItemId(element) {
- const item = element.closest(this.itemSelector);
- if (!item) return null;
-
- // Try common ID attributes in order
- return item.dataset.id ||
- item.dataset.itemId ||
- item.dataset.uploadId ||
- item.id;
- }
-
- /**
- * Check if an item is selected
- */
isSelected(id) {
return this.selectedItems.has(id);
}
- /**
- * Programmatically select specific items
- */
- select(ids) {
- const idArray = Array.isArray(ids) ? ids : [ids];
-
- idArray.forEach(id => {
- this.selectedItems.add(id);
-
- // Update checkbox if element exists
- const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
- if (item) {
- const checkbox = item.querySelector(this.checkboxSelector);
- if (checkbox) checkbox.checked = true;
- }
- });
-
- this.updateSelectionUI();
-
- this.notify('item-selected', {
- selectedItem: id,
- selectedItems: this.selectedItems,
- container: this.container
- });
+ getSelection() {
+ return new Set(this.selectedItems);
}
- /**
- * Programmatically deselect specific items
- */
- deselect(ids) {
- const idArray = Array.isArray(ids) ? ids : [ids];
-
- idArray.forEach(id => {
- this.selectedItems.delete(id);
-
- // Update checkbox if element exists
- const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
- if (item) {
- const checkbox = item.querySelector(this.checkboxSelector);
- if (checkbox) checkbox.checked = false;
- }
- });
-
- this.updateSelectionUI();
-
- this.notify('item-deselected', {
- selectedItem: id,
- selectedItems: this.selectedItems,
- container: this.container
- });
+ /*******************************************************************
+ * DOM HELPERS
+ *******************************************************************/
+ setCheckboxState(id, checked) {
+ const item = this.container.querySelector(`[data-upload-id="${id}"]`);
+ const checkbox = item?.querySelector(this.selectors.checkbox);
+ if (checkbox && checkbox.checked !== checked) {
+ checkbox.checked = checked;
+ }
}
- /**
- * Event system
- */
+ updateSelectionUI() {
+ if (!this.lastClicked || !this.ui.count) return;
+
+ const count = this.selectedItems.size;
+
+ // Update bulk controls visibility
+ let controls = this.lastClicked.querySelector(this.selectors.bulkControls);
+ if (controls) {
+ controls.hidden = count === 0;
+ }
+
+ // Update count display
+ let countEl = this.lastClicked.querySelector(this.selectors.count);
+ if (countEl) {
+ const itemText = count === 1 ? 'item' : 'items';
+ countEl.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+ countEl.hidden = count === 0;
+ }
+ }
+ /*******************************************************************
+ * EVENT SYSTEM
+ *******************************************************************/
subscribe(callback) {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
}
notify(event, data) {
- this.subscribers.forEach(cb => cb(event, data));
+ this.subscribers.forEach(cb => {
+ try {
+ cb(event, data);
+ } catch (e) {
+ console.error('HandleSelection subscriber error:', e);
+ }
+ });
}
- /**
- * Clean up event listeners
- */
destroy() {
- // Remove event listeners
- if (this.container) {
- this.container.removeEventListener('click', this.clickHandler);
- this.container.removeEventListener('change', this.changeHandler);
- this.container.removeEventListener('keydown', this.keyHandler);
- }
-
- // Clear selections
- this.clearSelection();
-
- // Clear subscribers
+ this.container.removeEventListener('change', this.changeHandler);
+ this.container.removeEventListener('click', this.clickHandler);
+ this.container.removeEventListener('keydown', this.keyHandler);
this.subscribers.clear();
-
- // Clear references
- this.container = null;
- this.ui = null;
- this.lastSelected = null;
+ this.selectedItems.clear();
}
}
-// Export for use in other modules
window.jvbHandleSelection = HandleSelection;
diff --git a/assets/js/concise/HandleSelectionOld.js b/assets/js/concise/HandleSelectionOld.js
new file mode 100644
index 0000000..e2d619c
--- /dev/null
+++ b/assets/js/concise/HandleSelectionOld.js
@@ -0,0 +1,392 @@
+/**
+ * HandleSelection - Reusable selection management for items in grids
+ *
+ * Handles selection logic including:
+ * - Individual item selection/deselection
+ * - Select all / Clear all
+ * - Range selection (shift+click)
+ * - Selection count updates
+ * - Bulk action visibility
+ *
+ * @class HandleSelection
+ */
+class HandleSelection {
+ /**
+ * @param {Object} config - Configuration object
+ * @param {HTMLElement} config.container - Container element holding items
+ * @param {Object} config.ui - UI element references
+ * @param {HTMLElement} config.ui.selectAll - Select all checkbox
+ * @param {HTMLElement} config.ui.bulkControls - Bulk actions container
+ * @param {HTMLElement} config.ui.count - Selection count display
+ * @param {string} config.itemSelector - Selector for individual items
+ * @param {string} config.checkboxSelector - Selector for item checkboxes
+ * @param {Function} config.onSelectionChange - Optional callback when selection changes
+ */
+ constructor(config) {
+
+ this.container = config.container;
+ this.ui = config.ui || {};
+ this.itemSelector = config.itemSelector || '.item';
+ this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]';
+
+ this.selectedItems = new Set();
+ this.lastSelected = null;
+ this.subscribers = new Set();
+
+ this.init();
+ }
+
+ init() {
+ // Bind event handlers
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.keyHandler = this.handleKeys.bind(this);
+
+ // Attach listeners
+ this.ui.wrapper.addEventListener('click', this.clickHandler);
+ this.ui.wrapper.addEventListener('change', this.changeHandler);
+ this.ui.wrapper.addEventListener('keydown', this.keyHandler);
+ }
+
+ handleKeys(e) {
+ // Ctrl/Cmd + A: Select all
+ if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
+ e.preventDefault();
+
+ if (this.ui.selectAll) {
+ this.ui.selectAll.checked = true;
+ this.selectAll(true);
+ if (window.jvbA11y) {
+ window.jvbA11y.announce('All items selected');
+ }
+ }
+ }
+
+ // Escape: Deselect all
+ if (e.key === 'Escape' && this.selectedItems.size > 0) {
+ this.selectAll(false);
+ if (window.jvbA11y) {
+ window.jvbA11y.announce('Selection cleared');
+ }
+ }
+
+ // Delete/Backspace: Remove selected items
+ if ((e.key === 'Delete' || e.key === 'Backspace') &&
+ !e.target.matches('input, textarea')
+ && this.selectedItems > 0) {
+ e.preventDefault();
+ if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) {
+ this.deselect(this.selectedItems);
+ }
+ }
+ }
+
+ handleClick(e) {
+ const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`);
+ if (!checkbox) return;
+
+ // Get the actual checkbox element
+ const input = checkbox.tagName === 'LABEL'
+ ? document.getElementById(checkbox.getAttribute('for'))
+ : checkbox;
+
+ if (!input) return;
+
+ // Handle shift+click for range selection
+ if (e.shiftKey && this.lastSelected) {
+ e.preventDefault();
+ this.handleRangeSelection(input);
+ } else {
+ // Store last clicked for range selection
+ const item = input.closest(this.itemSelector);
+ if (item) {
+ this.lastSelected = item;
+ }
+ }
+ }
+
+ handleChange(e) {
+ if (this.ui.selectAll && e.target === this.ui.selectAll) {
+ this.selectAll(e.target.checked);
+ } else {
+ const checkbox = e.target.closest(this.checkboxSelector);
+ if (!checkbox) return;
+
+ this.toggleSelection(this.getItemId(checkbox));
+ }
+ }
+
+ /**
+ * Toggle selection state of an item
+ */
+ toggleSelection(id) {
+ if (!id) return;
+
+ let selected = true;
+ if (this.selectedItems.has(id)) {
+ selected = false;
+ this.selectedItems.delete(id);
+ } else {
+ this.selectedItems.add(id);
+ }
+
+ if (selected) {
+ this.notify('item-selected', {
+ selectedItem: id,
+ selectedItems: this.selectedItems,
+ container: this.container
+ });
+ } else {
+ this.notify('item-deselected', {
+ selectedItem: id,
+ selectedItems: this.selectedItems,
+ container: this.container
+ });
+ }
+
+
+ this.updateSelectionUI();
+ }
+
+ /**
+ * Select or deselect all items
+ */
+ selectAll(checked) {
+ const items = this.container.querySelectorAll(this.itemSelector);
+
+ if (!checked) {
+ this.selectedItems.clear();
+ if (this.ui.selectAll) {
+ this.ui.selectAll.checked = false;
+ }
+ }
+
+ items.forEach(item => {
+ const id = this.getItemId(item);
+ const checkbox = item.querySelector(this.checkboxSelector);
+
+ if (checkbox) {
+ checkbox.checked = checked;
+ }
+
+ if (checked && id) {
+ this.selectedItems.add(id);
+ }
+ });
+
+ this.notify('select-all', {
+ container: this.container,
+ selected: checked,
+ items: items
+ })
+
+ this.updateSelectionUI();
+ }
+
+ /**
+ * Clear all selections
+ */
+ clearSelection() {
+ this.selectAll(false);
+ }
+
+ /**
+ * Handle shift+click range selection
+ */
+ handleRangeSelection(currentCheckbox) {
+ if (!this.lastSelected) {
+ this.lastSelected = currentCheckbox.closest(this.itemSelector);
+ return;
+ }
+
+ const currentItem = currentCheckbox.closest(this.itemSelector);
+ if (!currentItem) return;
+
+ // Get all items
+ const allItems = Array.from(this.container.querySelectorAll(this.itemSelector));
+
+ // Find indices
+ const lastIndex = allItems.indexOf(this.lastSelected);
+ const currentIndex = allItems.indexOf(currentItem);
+
+ if (lastIndex === -1 || currentIndex === -1) return;
+
+ // Determine range (handle both directions)
+ const startIndex = Math.min(lastIndex, currentIndex);
+ const endIndex = Math.max(lastIndex, currentIndex);
+
+ let isChecked = !currentCheckbox.checked;
+ // Select all items in range
+ for (let i = startIndex; i <= endIndex; i++) {
+ const item = allItems[i];
+ const checkbox = item.querySelector(this.checkboxSelector);
+ const id = this.getItemId(item);
+
+ if (checkbox && id) {
+ checkbox.checked = isChecked;
+ this.selectedItems.add(id);
+ }
+ }
+
+ // Update last selected to current
+ this.lastSelected = currentItem;
+
+ this.updateSelectionUI();
+
+ this.notify('range-selected', {
+ selectedItems: this.selectedItems,
+ container: this.container
+ });
+
+ // Announce for accessibility
+ const selectedCount = endIndex - startIndex + 1;
+ if (window.jvbA11y) {
+ window.jvbA11y.announce(`Selected ${selectedCount} items in range`);
+ }
+ }
+
+ /**
+ * Update selection UI elements
+ */
+ updateSelectionUI() {
+ const count = this.selectedItems.size;
+ const totalItems = this.container.querySelectorAll(this.itemSelector).length;
+
+ // Update bulk controls visibility
+ if (this.ui.bulkControls) {
+ this.ui.bulkControls.hidden = count === 0;
+ }
+
+ // Update count display
+ if (this.ui.count) {
+ const itemText = count === 1 ? 'item' : 'items';
+ this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+ this.ui.count.hidden = count === 0;
+ }
+
+ // Update select all checkbox state
+ if (this.ui.selectAll) {
+ this.ui.selectAll.checked = totalItems > 0 && count === totalItems;
+ this.ui.selectAll.indeterminate = count > 0 && count < totalItems;
+
+ // Update label text if available
+ const label = this.ui.selectAll.nextElementSibling ||
+ this.ui.selectAll.previousElementSibling;
+ if (label && label.tagName === 'LABEL') {
+ label.textContent = (totalItems > 0 && count === totalItems)
+ ? 'Clear Selection'
+ : 'Select All';
+ }
+ }
+ }
+
+ /**
+ * Get item ID from element
+ */
+ getItemId(element) {
+ const item = element.closest(this.itemSelector);
+ if (!item) return null;
+
+ // Try common ID attributes in order
+ return item.dataset.id ||
+ item.dataset.itemId ||
+ item.dataset.uploadId ||
+ item.id;
+ }
+
+ /**
+ * Check if an item is selected
+ */
+ isSelected(id) {
+ return this.selectedItems.has(id);
+ }
+
+ /**
+ * Programmatically select specific items
+ */
+ select(ids) {
+ const idArray = Array.isArray(ids) ? ids : [ids];
+
+ idArray.forEach(id => {
+ this.selectedItems.add(id);
+
+ // Update checkbox if element exists
+ const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+ if (item) {
+ const checkbox = item.querySelector(this.checkboxSelector);
+ if (checkbox) checkbox.checked = true;
+ }
+ });
+
+ this.updateSelectionUI();
+
+ this.notify('item-selected', {
+ selectedItem: id,
+ selectedItems: this.selectedItems,
+ container: this.container
+ });
+ }
+
+ /**
+ * Programmatically deselect specific items
+ */
+ deselect(ids) {
+ const idArray = Array.isArray(ids) ? ids : [ids];
+
+ idArray.forEach(id => {
+ this.selectedItems.delete(id);
+
+ // Update checkbox if element exists
+ const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`);
+ if (item) {
+ const checkbox = item.querySelector(this.checkboxSelector);
+ if (checkbox) checkbox.checked = false;
+ }
+ });
+
+ this.updateSelectionUI();
+
+ this.notify('item-deselected', {
+ selectedItem: id,
+ selectedItems: this.selectedItems,
+ container: this.container
+ });
+ }
+ /**
+ * Event system
+ */
+ subscribe(callback) {
+ this.subscribers.add(callback);
+ return () => this.subscribers.delete(callback);
+ }
+
+ notify(event, data) {
+ this.subscribers.forEach(cb => cb(event, data));
+ }
+
+ /**
+ * Clean up event listeners
+ */
+ destroy() {
+ // Remove event listeners
+ if (this.container) {
+ this.container.removeEventListener('click', this.clickHandler);
+ this.container.removeEventListener('change', this.changeHandler);
+ this.container.removeEventListener('keydown', this.keyHandler);
+ }
+
+ // Clear selections
+ this.clearSelection();
+
+ // Clear subscribers
+ this.subscribers.clear();
+
+ // Clear references
+ this.container = null;
+ this.ui = null;
+ this.lastSelected = null;
+ }
+}
+
+// Export for use in other modules
+window.jvbHandleSelection = HandleSelection;
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index d0fb19b..7e3033c 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -163,6 +163,7 @@
method: 'POST',
headers: {},
data: {},
+ sendNow: false, // true = process immediately
canMerge: true,
popup: 'Saving changes...',
title: 'Operation',
@@ -183,6 +184,14 @@
return null;
}
+ if (item.sendNow) {
+ this.processOperation(item).then(()=> {});
+ this.store.clearCache();
+ window.debouncer.schedule('fastQueue', this.startPolling.bind(this), 200);
+ this.showQueue();
+ return item.id;
+ }
+
const existingOps = Array.from(this.store.data.values()).filter(op=>
op.status === 'queued' &&
op.endpoint === item.endpoint &&
@@ -201,7 +210,6 @@
return existing.id;
}
- console.log('Added to Queue: ', item);
this.store.clearCache();
//Add new operation to DataStore
@@ -313,12 +321,14 @@
this.maybeStartPolling() ? this.showQueue() : this.hideQueue();
}
- async processOperation(operation) {
+ async processOperation(operation, skip = false) {
try {
- this.updateOperationStatus(operation.id, 'uploading');
+ if (!skip) {
+ this.updateOperationStatus(operation.id, 'uploading');
- if (operation.data?._isFormData) {
- operation.data = await this.store.objectToFormData(operation.data);
+ if (operation.data?._isFormData) {
+ operation.data = await this.store.objectToFormData(operation.data);
+ }
}
const url = `${this.config.apiBase}${operation.endpoint}`;
@@ -336,7 +346,6 @@
});
operation.headers['Content-Type'] = 'application/json';
}
-
const response = await fetch(url, {
method: operation.method,
headers: operation.headers,
@@ -344,7 +353,9 @@
});
const result = await response.json();
-
+ if (skip) {
+ operation.data = {};
+ }
if (response.ok && result.success !== false) {
// Handle server-side merge
if (result.id && operation.id !== result.id) {
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index c8f39f7..b48d38c 100644
--- a/assets/js/concise/UploadManager.js
+++ b/assets/js/concise/UploadManager.js
@@ -1,102 +1,1074 @@
-/**
- * UploadManager - Refactored for clarity
- *
- * Architecture:
- * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
- * - Maps (uploadElements, fieldElements) = Runtime DOM references
- * - Upload data flows: File → Process → Queue → Server → Clean up stores
- */
class UploadManager {
constructor() {
- // Load dependencies
- this.queue = window.jvbQueue;
this.a11y = window.jvbA11y;
+ this.queue = window.jvbQueue;
this.error = window.jvbError;
- this.fieldStoreReady = false;
- this.uploadStoreReady = false;
- this.hasCheckedForUploads = false;
- const {fields, uploads} = window.jvbStore.register(
+
+ this.subscribers = new Set();
+
+ this.initStores();
+ this.initWorker();
+
+ //Maps for DOM references
+ this.fields = new Map();
+ this.uploads = new Map();
+ this.groups = new Map();
+
+ this.selected = new Map();
+ this.selectionHandlers = new Map();
+ this.sortables = new Map();
+
+ this.previewUrls = new Set();
+ this.initElements();
+ this.initListeners();
+ }
+
+ initStores() {
+ const {uploads, groups} = window.jvbStore.register(
'uploads',
[
{
- storeName: 'fields',
- keyPath: 'id',
- indexes: [
- { name: 'fieldId', keyPath: 'fieldId' },
- { name: 'timestamp', keyPath: 'timestamp' },
- { name: 'content', keyPath: 'content' },
- { name: 'itemId', keyPath: 'itemId' },
- { name: 'status', keyPath: 'status' }
- ],
- TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
- delayFetch: true
- },
- {
storeName: 'uploads',
keyPath: 'id',
- storeBlobs: true,
indexes: [
- { name: 'fieldId', keyPath: 'fieldId' },
+ { name: 'field', keyPath: 'field' },
{ name: 'status', keyPath: 'status' },
- { name: 'groupId', keyPath: 'groupId' },
- { name: 'attachmentId', keyPath: 'attachmentId' }
+ { name: 'group', keyPath: 'group' },
+ { name: 'src', keyPath: 'src' }
],
- delayFetch: true
+ },
+ {
+ storeName: 'groups',
+ keyPath: 'id',
+ indexes: [
+ { name: 'field', keyPath: 'field' },
+ { name: 'src', keyPath: 'src' }
+ ]
}
]
);
- this.fieldStore = fields;
- this.uploadStore = uploads;
- window.jvbUploadBlobs = this.uploadStore;
+ this.stores = {
+ uploads: uploads,
+ groups: groups,
+ ready: []
+ };
- // Subscribe to store events
- this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
- this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+ this.stores.uploads.subscribe(this.handleStores.bind(this, 'uploads'));
+ this.stores.groups.subscribe(this.handleStores.bind(this, 'groups'));
+ this.queue.subscribe((event, operation) => {
+ if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+ return;
+ }
- // RUNTIME DATA - DOM references and ephemeral state
- this.uploadElements = new Map(); // uploadId → { element, preview, location }
- this.fieldElements = new Map(); // fieldId → { element, ui, config }
- this.groupElements = new Map(); // groupId → { element, grid, fieldId }
- // Selection and UI state
- this.selected = new Map();
- this.selectionHandlers = new Map();
- this.previewUrls = new Set();
- this.sortableInstances = new Map();
+ const fieldId = operation.data instanceof FormData
+ ? operation.data.get('fieldId')
+ : operation.data?.fieldId;
+ if (!fieldId) {
+ return;
+ }
+ switch (event) {
+ case 'cancel-operation':
+ this.handleOperationCancelled(fieldId).then(()=>{});
+ break;
+ case 'operation-status':
+ this.handleFieldStatus(fieldId, operation).then(()=>{});
+ break;
+ case 'operation-completed':
+ this.handleOperationComplete(operation, fieldId).then(()=>{});
+ break;
+ case 'operation-failed':
+ case 'operation-failed-permanent':
+ this.handleOperationFailed(operation, fieldId).then(()=>{});
+ break;
+ }
+ });
+ }
- // Worker for image processing
- this.initWorker();
+ storesReady() {
+ return this.stores.ready.length === 2;
+ }
- // Notification subscribers
- this.subscribers = new Set();
+ handleStores(storeName, event) {
+ if (event === 'data-ready') {
+ this.stores.ready.push(storeName);
+ if (this.storesReady()) {
+ this.checkRecovery();
+ }
+ }
+ }
- // Selectors
+ initWorker() {
+ this.worker = null;
+ this.workerState = {
+ worker: null,
+ tasks: new Map(),
+ restart: { count: 0, max: 3 },
+ settings: {
+ timeout: 3000,
+ maxConcurrent: 3,
+ restartAfterTimeout: true
+ }
+ };
+ }
+
+ initElements() {
this.selectors = {
- field: {
+ fields: {
field: '[data-upload-field]',
input: 'input[type="file"]',
dropZone: '.file-upload-container',
- preview: '.item-grid.preview',
- progress: '.image-progress'
+ preview: '.preview-wrap',
+ grid: '.item-grid.preview',
+ progress: {
+ progress: '.file-upload-container .progress',
+ fill: '.file-upload-container .progress .fill',
+ details: '.file-upload-container .progress .details',
+ icon: '.file-upload-container .progress .icon'
+ },
+ selectAll: '[name="select-all-uploads"]',
+ actions: '.selection-actions',
+ count: '.selection-count',
+ hidden: 'input[type="hidden"]'
},
+ // groups = selectors that affect groups as a whole
groups: {
- container: '.upload-group',
- grid: '.item-grid.group',
- header: '.group-header',
+ container: '.group-display',
+ grid: '.item-grid.groups',
+ empty: '.empty-group',
+ header: '.sidebar .header',
+ },
+ // group = selectors that affect individual groups
+ group: {
+ item: '.upload-group',
+ actions: '.selection-actions',
selectAll: '[name="select-all-group"]',
- actions: '.group-actions',
- count: '.selection-controls .info'
+ count: '.group-header .info',
+ fields: 'details .fields',
+ grid: '.item-grid.group',
+ total: '.group-content .group-count'
},
items: {
item: '[data-upload-id]',
checkbox: '[name*="select-item"]',
featured: '[name="featured"]',
- details: 'details'
+ image: 'img',
+ details: 'details',
+ progress: {
+ progress: '.progress',
+ fill: '.fill',
+ details: '.details',
+ icon: '.icon'
+ }
+ }
+ };
+ }
+
+ initListeners() {
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.dragEnterHandler = this.handleDragEnter.bind(this);
+ this.dragLeaveHandler = this.handleDragLeave.bind(this);
+ this.dragOverHandler = this.handleDragOver.bind(this);
+ this.dropHandler = this.handleDrop.bind(this);
+
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ document.addEventListener('dragenter', this.dragEnterHandler);
+ document.addEventListener('dragleave', this.dragLeaveHandler);
+ document.addEventListener('dragover', this.dragOverHandler);
+ document.addEventListener('drop', this.dropHandler);
+
+ window.addEventListener('beforeunload', () => {
+ this.cleanupAllPreviewUrls();
+ });
+ }
+
+ async setUpload(uploadId, data) {
+ const defaults = {
+ id: uploadId,
+ attachment: null,
+ group: null,
+ field: null,
+ src: window.location.href,
+ blob: null,
+ status: 'local_processing',
+ operationId: null,
+ fields: {}
+ };
+
+ const upload = { ...defaults, ...data };
+ Object.preventExtensions(upload);
+ await this.stores.uploads.save(upload);
+ return upload;
+ }
+
+ /*********************************************************************
+ UTILITY
+ *********************************************************************/
+ createPreviewUrl(file) {
+ const url = URL.createObjectURL(file);
+ this.previewUrls.add(url);
+ return url;
+ }
+ revokePreviewUrl(url) {
+ if (url?.startsWith('blob:')) {
+ URL.revokeObjectURL(url);
+ this.previewUrls.delete(url);
+ }
+ }
+
+ formatFile(upload) {
+ if (!upload.blob) return null;
+ return new File([upload.blob], upload.fields.originalName || 'file', {
+ type: upload.fields.type || upload.blob.type,
+ lastModified: upload.fields.lastModified || Date.now()
+ });
+ }
+ /*********************************************************************
+ LISTENERS
+ *********************************************************************/
+ handleClick(e) {
+ //Open the file input if it's a dropzone
+ let dropZone = window.targetCheck(e, this.selectors.fields.dropZone);
+ if (dropZone && !e.target.matches('input, button, a')){
+ dropZone.querySelector(this.selectors.fields.input)?.click();
+ }
+
+ //Handle action buttons
+ const button = window.targetCheck(e, '[data-action]');
+ if (button) this.handleAction(button);
+ }
+ handleAction(button) {
+ const action = button.dataset.action;
+ const fieldId = this.getFieldIdFromElement(button);
+
+ switch (action) {
+ case 'add-to-group':
+ this.handleAddToGroup(fieldId).then(()=>{});
+ break;
+ case 'delete-group':
+ this.handleDeleteGroup(button);
+ break;
+ case 'delete-upload':
+ case 'remove-from-group':
+ this.handleRemoveItem(button).then(()=>{});
+ break;
+ case 'upload':
+ this.queueUploads('uploads/groups',fieldId).then(()=>{});
+ break;
+ case 'restore':
+ this.handleRestoreSelected().then(()=>{});
+ break;
+ case 'restore-all':
+ this.handleRestoreAll().then(()=>{});
+ break;
+ case 'clear-cache':
+ this.handleClearCache().then(()=>{});
+ break;
+ }
+ }
+ handleChange(e) {
+ let fieldId = this.getFieldIdFromElement(e.target);
+ if (!fieldId) return;
+
+ if (e.target.matches(this.selectors.fields.input)) {
+ const files = Array.from(e.target.files);
+ if (files.length > 0) this.processFiles(fieldId, files).then(()=>{});
+ return;
+ }
+
+ // Skip selection-related inputs
+ if (e.target.matches(this.selectors.items.checkbox) ||
+ e.target.matches(this.selectors.items.featured) ||
+ e.target.matches('[name*="select-"]')) {
+ return;
+ }
+
+ let field = this.fields.get(fieldId);
+ if (!field || !field.config.autoUpload) return;
+
+ if (field.config.destination === 'post_group') {
+ this.handleGroupMetaChange(e.target);
+ } else {
+ this.queueUploadMeta(e).then(()=>{});
+ }
+ }
+ handleGroupMetaChange(input) {
+ const element = input.closest(this.selectors.group.fields);
+ if (!element) return;
+
+ const groupId = element.dataset.groupId;
+ const group = this.stores.groups.get(groupId); // Changed from this.groups
+ if (!group) return;
+
+ window.debouncer.schedule(`group-meta-${groupId}`, async (input, groupId) => {
+ let name = input.name
+ .replace(`${groupId}_`, '')
+ .replace(`${groupId}[`, '')
+ .replace(']', '');
+ group.fields[name] = input.value;
+ await this.setGroup(groupId, group);
+ }, 300);
+ }
+ handleDragEnter(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.fields.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ dropZone.classList.add('dragover');
+ }
+ }
+ handleDragLeave(e) {
+ const dropZone = e.target.closest(this.selectors.fields.dropZone);
+ if (dropZone && !dropZone.contains(e.relatedTarget)) {
+ dropZone.classList.remove('dragover');
+ }
+ }
+ handleDragOver(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.fields.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ }
+ }
+ handleDrop(e) {
+ const dropZone = e.target.closest(this.selectors.fields.dropZone);
+ if (!dropZone) return;
+
+ e.preventDefault();
+ dropZone.classList.remove('dragover');
+ dropZone.classList.add('uploading');
+
+
+ const files = Array.from(e.dataTransfer.files);
+ if (files.length === 0) return;
+
+ const fieldId = this.getFieldIdFromElement(dropZone);
+ if (fieldId) {
+ this.processFiles(fieldId, files).then(()=>{});
+ this.a11y.announce(`${files.length} file(s) dropped for upload`);
+ }
+ }
+
+ async queueUploads(endpoint, fieldId) {
+ let data = new FormData();
+ const field = this.fields.get(fieldId);
+ if (!field) return;
+
+ let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ if (uploads.length === 0) return;
+
+ const [ isUpload, isGroups] =
+ [ endpoint === 'uploads', endpoint === 'uploads/groups'];
+
+ data.append('fieldId', field.id);
+ data.append('content', field.config.content);
+
+ if (isUpload) {
+ data.append('mode', field.config.mode);
+ data.append('field_name', field.config.name);
+ data.append('fieldId', field.id);
+ data.append('field_type', field.config.type);
+ data.append('subtype', field.config.subtype);
+ data.append('item_id', field.config.itemID);
+ data.append('destination', field.config.destination);
+ }
+
+ let posts, uploadMap, files;
+ if (isGroups) {
+ ({posts, uploadMap, files} = this.collectGroups(fieldId));
+ } else if (isUpload) {
+ ({uploadMap, files} = this.collectUploads(fieldId));
+ }
+
+ if (isGroups) {
+ data.append('posts', JSON.stringify(posts));
+ }
+ files.forEach(file => {
+ data.append('files[]', file);
+ });
+ data.append('upload_ids', JSON.stringify(uploadMap));
+
+ let title, popup;
+ if (isUpload) {
+ title = `Uploading ${uploads.length} file${uploads.length>1?'s':''} to server...`;
+ popup = `Uploading ${uploads.length} file${uploads.length>1?'s':''}...`;
+ } else if (isGroups) {
+ title = `Creating ${posts.length} ${field.config.content}${posts.length > 1 ? 's' : ''} from uploads...`;
+ popup = `Creating ${posts.length} post${posts.length>1?'s':''}...`;
+ }
+ await this.setBulkUpload(uploads, 'status', 'queued');
+ let operationId = this.sendToQueue(endpoint, data, title, popup);
+
+ if (endpoint === 'uploads/groups') {
+ let details = field.element.closest('details');
+ if (details) {
+ details.open = false;
+ }
+ }
+ if (operationId) {
+ field.operationId = operationId;
+ await this.setBulkUpload(uploads, 'operationId', operationId);
+ await this.setBulkUpload(uploads, 'status', 'uploading');
+ await this.setBulkGroup(fieldId, 'operationId', operationId);
+ this.fields.set(field.id, field);
+ } else {
+ await this.setBulkUpload(uploads, 'status', 'failed');
+ }
+ this.notify('sent-to-queue', fieldId);
+ return operationId;
+ }
+
+ async sendToQueue(endpoint, data, title = '', popup = '', mergable = false) {
+ if (popup === '') {
+ popup = title;
+ }
+ const operation = {
+ endpoint: endpoint,
+ method: 'POST',
+ data: data,
+ title: title,
+ popup: popup,
+ canMerge: mergable,
+ sendNow: endpoint === 'uploads/groups',
+ headers: {
+ 'action_nonce': window.auth.getNonce('dash')
+ },
+ append: '_upload'
+ }
+ try {
+ return await this.queue.addToQueue(operation);
+ } catch (error) {
+ this.error.log(error, {
+ component: 'UploadManager',
+ action: 'sentToQueue'
+ });
+ return false;
+ }
+ }
+
+ collectGroups(fieldId) {
+ let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ let groups = this.stores.groups.filterByIndex({field: fieldId});
+
+ let posts = [];
+ let uploadMap = [];
+ let files = [];
+
+ for (const group of groups) {
+ const post = {
+ images: [],
+ fields: group.fields??{}
+ };
+
+ const groupUploads = uploads.filter(u => u.group === group.id);
+ for (const upload of groupUploads) {
+ const file = this.formatFile(upload);
+ if (file) {
+ files.push(file);
+ const imageData = {
+ upload_id: upload.id,
+ index: uploadMap.length
+ };
+ let uploadEl = this.uploads.get(upload.id);
+ if (uploadEl.ui?.featured?.checked) {
+ post.fields.featured = upload.id;
+ }
+ post.images.push(imageData);
+ uploadMap.push(upload.id);
+ }
+ }
+ posts.push(post);
+ }
+
+ const remaining = uploads.filter(u => !u.group);
+
+ for (const upload of remaining) {
+ const post = {
+ images: [],
+ fields: {}
+ };
+
+ const file = this.formatFile(upload);
+ if (file) {
+ files.push(file);
+
+ const imageData = {
+ upload_id: upload.id,
+ index: uploadMap.length
+ };
+ post.images.push(imageData);
+ uploadMap.push(upload.id);
+ }
+ posts.push(post);
+ }
+ return {posts, uploadMap, files};
+ }
+
+ collectUploads(fieldId) {
+ let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ if (uploads.length === 0) return;
+
+ let uploadMap = [];
+ let files = [];
+
+ for (const upload of uploads) {
+ const file = this.formatFile(upload);
+ if (file) {
+ files.push(file);
+ uploadMap.push(upload.id);
+ }
+ }
+ return { uploadMap, files };
+ }
+
+ async queueUploadMeta(e) {
+ const uploadId = e.target.closest(this.selectors.items.item)?.dataset.uploadId;
+ const upload = this.stores.uploads.get(uploadId);
+ if (!uploadId || !upload) return;
+
+ const field = this.fields.get(upload.field);
+ if (!field) return;
+
+ let data = {};
+ data[e.target.name] = e.target.value;
+
+ upload.fields = { ...upload.fields, ...data };
+ await this.setUpload(upload.id, upload);
+
+ let queueData = {};
+ queueData[upload.attachmentId ?? upload.id] = upload.fields;
+ return await this.sendToQueue('uploads/meta', queueData, 'Uploading Meta', '', true);
+ }
+
+ async handleOperationComplete(operation, fieldId) {
+ const response = operation.response;
+
+ // Handle direct upload results (from uploads endpoint)
+ if (response?.data) {
+ const results = Array.isArray(response.data) ? response.data : Object.values(response.data);
+ for (const result of results) {
+ if (result.upload_id && result.attachment_id) {
+ const upload = this.stores.uploads.get(result.upload_id);
+ if (upload) {
+ upload.attachmentId = result.attachment_id;
+ upload.status = 'completed';
+ await this.stores.uploads.save(upload);
+ }
+ }
+ }
+ }
+
+ // Clear completed uploads and groups
+ const uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ const groups = this.stores.groups.filterByIndex({field: fieldId});
+
+ await Promise.all([
+ ...uploads
+ .filter(upload => upload.status === 'completed')
+ .map(upload => this.clearUpload(upload.id)),
+ ...groups.map(group => this.stores.groups.delete(group.id))
+ ]);
+
+ this.notify('uploads-complete', { fieldId, response });
+ }
+ /*********************************************************************
+ FIELD LOGIC
+ *********************************************************************/
+ scanFields(container, autoUpload = true) {
+ const fields = container.querySelectorAll(this.selectors.fields.field);
+ fields.forEach(uploader => this.registerField(uploader, autoUpload));
+ }
+
+ registerField(element, autoUpload = true, id = null) {
+ const data = {
+ element: element,
+ id: (id) ? id : this.determineFieldId(element),
+ config: this.extractFieldConfig(element, autoUpload),
+ uploads: new Set(),
+ operationId: null,
+ groups: [],
+ ui: window.uiFromSelectors(this.selectors.fields, element),
+ groupUI: window.uiFromSelectors(this.selectors.groups, element)
+ };
+
+
+ this.fields.set(data.id, data);
+
+ element.dataset.uploader = data.id;
+ this.getSelectionHandler(data.id);
+ if (data.config.type !== 'single') {
+ this.initSortable(data.id);
+ }
+
+ return data.id;
+ }
+
+ extractFieldConfig(fieldElement, autoUpload) {
+ return {
+ autoUpload: autoUpload,
+ destination: fieldElement.dataset.destination || 'meta', //TODO: why do we need this?
+ content: this.extractFieldContent(fieldElement),
+ mode: fieldElement.dataset.mode || 'direct',
+ type: fieldElement.dataset.type || 'single',
+ name: fieldElement.dataset.field,
+ itemID: this.extractFieldItemId(fieldElement)??0,
+ maxFiles: parseInt(fieldElement.dataset.maxFiles)??25,
+ subType: fieldElement.dataset.subtype?? 'image'
+ };
+ }
+
+ extractFieldContent(fieldElement) {
+ return fieldElement.dataset.content ||
+ fieldElement.closest('dialog')?.dataset.content ||
+ fieldElement.closest('form')?.dataset.save || null;
+ }
+ extractFieldItemId(fieldElement) {
+ return fieldElement.dataset.itemId ||
+ fieldElement.closest('dialog')?.dataset.itemId || null;
+ }
+
+ determineFieldId(fieldElement) {
+ let content = this.extractFieldContent(fieldElement);
+ content = (content === null) ? '' : content+'_';
+
+ let itemID = this.extractFieldItemId(fieldElement);
+ itemID = (itemID === null) ? '' : itemID+'_';
+
+ const field = fieldElement.dataset.field || '';
+
+ return `${content}${itemID}${field}`;
+ }
+
+ getFieldIdFromElement(el) {
+ const field = el.closest(this.selectors.fields.field);
+ return field?.dataset.uploader || null;
+ }
+
+ updateFieldProgress(fieldId, current, total, message) {
+ const field = this.fields.get(fieldId);
+ if (!field) return;
+ window.showProgress(field.ui.progress,current, total, message);
+ }
+ /*********************************************************************
+ IMAGE PROCESSING FILE PROCESSING
+ *********************************************************************/
+ getWorker() {
+ if (!this.workerState.worker && typeof OffscreenCanvas !== 'undefined') {
+ this.workerState.worker = new Worker('worker.js');
+ this.workerState.worker.onmessage = (e) => this.handleWorkerMessage(e);
+ this.workerState.worker.onerror = (e) => this.handleWorkerError(e);
+ }
+ return this.workerState.worker;
+ }
+
+ handleWorkerMessage(e) {
+ const { id, blob } = e.data;
+ const task = this.workerState.tasks.get(id);
+ if (task) {
+ clearTimeout(task.timeoutId);
+ task.resolve(blob);
+ this.workerState.tasks.delete(id);
+ }
+ }
+
+ handleWorkerError(e) {
+ // Reject all pending tasks
+ this.workerState.tasks.forEach(task => {
+ clearTimeout(task.timeoutId);
+ task.reject(e);
+ });
+ this.workerState.tasks.clear();
+ this.restartWorker();
+ }
+
+ restartWorker() {
+ if (this.workerState.worker) {
+ this.workerState.worker.terminate();
+ this.workerState.worker = null;
+ }
+ this.workerState.restart.count++;
+ }
+ async processImages(files, maxWidth = 2200, maxHeight = 2200){
+ const results = [];
+ const queue = [...files];
+ const concurrency = this.workerState.settings.maxConcurrent;
+
+ const processNext = async () => {
+ while (queue.length > 0) {
+ const file = queue.shift();
+ results.push(await this.processImage(file, maxWidth, maxHeight));
}
};
- this.statusMapping = {
+ await Promise.all(
+ Array.from({length: Math.min(concurrency, files.length)}, () => processNext())
+ );
+
+ return results;
+ }
+ async processImage(file, maxWidth = 2200, maxHeight = 2200, timeout = 3000){
+ if (typeof OffscreenCanvas=== 'undefined') {
+ return this.resizeImage(file,maxWidth,maxHeight);
+ }
+ try {
+ return await this.withTimeout(
+ this.workerImage(file, maxWidth, maxHeight),
+ timeout
+ );
+ } catch (e) {
+ return this.resizeImage(file, maxWidth, maxHeight);
+ }
+ }
+ withTimeout(promise, ms) {
+ return Promise.race([
+ promise,
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Timeout')), ms)
+ )
+ ]);
+ }
+
+
+ async workerImage(file, maxWidth = 2200, maxHeight = 2200) {
+ const { settings, restart } = this.workerState;
+
+ if (restart.count >= restart.max) {
+ throw new Error('Worker max restarts exceeded');
+ }
+
+ const bitmap = await createImageBitmap(file);
+
+ let { width, height } = bitmap;
+ if (width > maxWidth || height > maxHeight) {
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
+ width = Math.round(width * ratio);
+ height = Math.round(height * ratio);
+ }
+
+ const worker = this.getWorker();
+ const id = crypto.randomUUID();
+
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ this.workerState.tasks.delete(id);
+ if (settings.restartAfterTimeout) {
+ this.restartWorker();
+ }
+ reject(new Error('Timeout'));
+ }, settings.timeout);
+
+ this.workerState.tasks.set(id, { resolve, reject, timeoutId });
+
+ worker.postMessage(
+ { id, imageBitmap: bitmap, width, height, type: file.type, quality: 0.9 },
+ [bitmap]
+ );
+ });
+ }
+ resizeImage(file, maxWidth, maxHeight) {
+ return new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => {
+ URL.revokeObjectURL(img.src);
+ // Calculate new dimensions keeping aspect ratio
+ let { width, height } = img;
+
+ if (width > maxWidth || height > maxHeight) {
+ const ratio = Math.min(maxWidth / width, maxHeight / height);
+ width = Math.round(width * ratio);
+ height = Math.round(height * ratio);
+ }
+
+ // Draw to canvas at new size
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ canvas.getContext('2d').drawImage(img, 0, 0, width, height);
+
+ // Export as blob for upload
+ canvas.toBlob(resolve, file.type, 0.9);
+ };
+ img.src = URL.createObjectURL(file);
+ });
+ }
+
+ async processFiles(fieldId, files) {
+ let field = this.fields.get(fieldId);
+ if (!field) return;
+
+ if (field.groupUI.container) {
+ field.groupUI.container.hidden = false;
+ }
+
+ const totalFiles = files.length;
+ let processed = 0;
+
+ this.updateFieldProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+ // Create upload records for all files first
+ const uploadEntries = await Promise.all(
+ files.map(async (file) => {
+ const uploadId = window.generateID('upload');
+ const upload = await this.setUpload(uploadId, {
+ id: uploadId,
+ field: fieldId,
+ status: 'local_processing',
+ blob: null,
+ fields: {
+ originalName: file.name,
+ originalSize: file.size,
+ type: file.type,
+ lastModified: file.lastModified
+ }
+ });
+
+ const element = await this.createUpload(uploadId, file, fieldId);
+ this.uploads.set(uploadId, {
+ element: element,
+ ui: window.uiFromSelectors(this.selectors.items, element)
+ });
+
+ await this.addToGroup(uploadId, null);
+
+ return { uploadId, upload, file };
+ })
+ );
+
+ // Batch process images with concurrency control
+ const imageEntries = uploadEntries.filter(e => e.file.type.startsWith('image/'));
+ const otherEntries = uploadEntries.filter(e => !e.file.type.startsWith('image/'));
+
+ // Process images in batches
+ const processedBlobs = await this.processImages(
+ imageEntries.map(e => e.file)
+ );
+
+ // Update image uploads with processed blobs
+ for (let i = 0; i < imageEntries.length; i++) {
+ const { uploadId, upload } = imageEntries[i];
+ upload.blob = processedBlobs[i];
+ upload.fields.size = processedBlobs[i].size;
+ upload.status = 'queued';
+ await this.setUpload(uploadId, upload);
+ processed++;
+ this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+ }
+
+ // Handle non-image files (no processing needed)
+ for (const { uploadId, upload, file } of otherEntries) {
+ upload.blob = file;
+ upload.status = 'queued';
+ await this.setUpload(uploadId, upload);
+ processed++;
+ this.updateFieldProgress(fieldId, processed, totalFiles, 'Processing files...');
+ }
+
+ this.maybeLockUploads(fieldId);
+ if (field.config.autoUpload && field.config.destination !== 'post_group') {
+ await this.queueUploads('uploads', fieldId);
+ }
+ }
+ /*************************************************************
+ RECOVERY
+ *************************************************************/
+ async checkRecovery() {
+ const pendingUploads = this.stores.uploads.filterByIndex({status: ['local_processing', 'queued', 'uploading']});
+ if (pendingUploads.length === 0) return;
+
+ let notification = window.getTemplate('restoreNotification');
+ if (!notification) {
+ this.error.log(
+ 'No restore notification',
+ {
+ component: 'UploadManager',
+ src: window.location.href
+ }
+ );
+ return;
+ }
+ // Group by source page
+ const bySource = new Map();
+ pendingUploads.forEach(upload => {
+ const src = upload.src || 'unknown';
+ if (!bySource.has(src)) bySource.set(src, []);
+ bySource.get(src).push(upload);
+ });
+
+ const currentSrc = window.location.href;
+
+
+ let source = bySource.size > 1 ? ` across ${bySource.size} pages` : '';
+ let upload = pendingUploads.length > 1 ? 'uploads' : 'upload';
+ let message = `${pendingUploads.length} ${upload} can be recovered${source}`;
+
+ let details = notification.querySelector('.details');
+ if (details) {
+ details.textContent = message;
+ }
+
+ let i = 1;
+ for (const [src, uploads] of bySource) {
+ let template = window.getTemplate('restoreField');
+ if (!template) continue;
+ let fieldId = this.registerField(template,false, 'recovery_'+i);
+ let field = this.fields.get(fieldId);
+ i++;
+ let isCurrent = src === currentSrc;
+ let [
+ h3,
+ a,
+ grid
+ ] = [
+ template.querySelector('h3'),
+ template.querySelector('h3 a'),
+ template.querySelector('.item-grid')
+ ];
+
+ template.open = isCurrent;
+ if (!isCurrent) {
+ [a.href, a.title,a.textContent] =
+ [src, 'Navigate to Page and Restore', src];
+ } else {
+ a.remove();
+ h3.textContent = 'From this page:';
+ }
+
+ let filteredGroupIds = [...new Set(uploads.map(upload => upload.group??'preview'))];
+
+ for (let groupId of filteredGroupIds) {
+ let group = (groupId === 'preview') ? true : this.stores.groups.get(groupId);
+ if (!group) continue;
+
+ let groupElement = await this.createGroupElement(groupId,field.id);
+ let groupGrid = groupElement.querySelector('.item-grid');
+ let theseUploads = uploads.filter(upload => upload.group === (groupId === 'preview') ? null : groupId);
+ for (const [key, value] of Object.entries(group.fields ?? {})) {
+ let field = groupElement.querySelector(`input[name*="${key}"]`);
+ if (field) field.value = value;
+ }
+ for (let upload of theseUploads) {
+ let item = await this.createUpload(upload.id, this.formatFile(upload), field.id);
+ groupGrid.append(item);
+ }
+
+ grid.append(groupElement);
+ }
+ notification.querySelector('.wrap').append(template);
+ }
+ document.body.append(notification);
+ notification = document.querySelector('dialog.restore-uploads');
+ this.restoreModal = new window.jvbModal(notification);
+ this.restoreSelection = new window.jvbHandleSelection({
+ container: notification,
+ wrapper: '.restore-uploads .wrap',
+ bulkControls: '.selection-actions',
+ selectAll: '#select-all-restore',
+ count: '.selection-count'
+ });
+ this.restoreModal.handleOpen();
+ }
+
+ async handleRestoreSelected() {
+ if (!this.restoreSelection) return;
+
+ let selected = Array.from(this.restoreSelection.selectedItems);
+ if (selected.length === 0) {
+ return;
+ }
+
+ await this.restoreSelectedUploads(selected);
+ }
+ async handleRestoreAll() {
+ if (!this.restoreModal) return;
+ const allUploads = Array.from(this.restoreModal.modal.querySelectorAll('.item.upload')).map(item => item.dataset.uploadId);
+
+ await this.restoreSelectedUploads(allUploads);
+ }
+
+ async restoreSelectedUploads(selectedUploads) {
+ let currentPage = window.location.href;
+
+ let uploads = Array.from(this.stores.uploads.data.values()).filter(
+ upload => selectedUploads.includes(upload.id) && upload.src === currentPage
+ );
+
+ let groups = [... new Set(uploads.map(upload => upload.group))].filter(Boolean);
+
+ let fieldId = uploads[0].field;
+ let field = document.querySelector(`[data-uploader="${fieldId}"]`);
+ if (!field) {
+ console.log('No field found for '+fieldId);
+ return;
+ }
+ let fieldData = this.fields.get(fieldId);
+ if (fieldData.groupUI.container) {
+ fieldData.groupUI.container.hidden = false;
+ }
+
+ let usedIds = [];
+ for (let gr of groups) {
+
+ let group = this.stores.groups.get(gr);
+ await this.createGroup(fieldId,gr);
+ let element = this.groups.get(gr);
+
+ let theseUploads = uploads.filter(upload => upload.group === gr);
+ if (group && this.groups.has(gr)) {
+ let fields = group.fields;
+
+ for (const [key, value] of Object.entries(fields)) {
+ let fi = element.element.querySelector(`input[name*="${key}"]`);
+ if (fi) {
+ fi.value = value;
+ }
+ }
+ }else {
+ //Couldn't restore the group for some reason, just add it to the main preview grid instead
+ gr = null;
+ }
+
+ for (let upload of theseUploads) {
+ let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+ this.uploads.set(upload.id, {
+ element: item,
+ ui: window.uiFromSelectors(this.selectors.items, item)
+ });
+ await this.addToGroup(upload.id, gr);
+ usedIds.push(upload.id);
+ }
+
+ }
+
+ let remaining = uploads.filter(upload => !usedIds.includes(upload.id));
+ for (let upload of remaining) {
+ let item = await this.createUpload(upload.id, this.formatFile(upload), fieldId);
+ this.uploads.set(upload.id, {
+ element: item,
+ ui: window.uiFromSelectors(this.selectors.items, item)
+ });
+ await this.addToGroup(upload.id, null);
+ }
+
+ this.cleanupRestore();
+ }
+
+ cleanupRestore() {
+ this.restoreModal.handleClose();
+ this.restoreSelection.destroy();
+ this.restoreSelection = null;
+ this.restoreModal.destroy();
+ this.restoreModal.modal.remove();
+ this.restoreModal = null;
+ }
+ /*******************************************************************************
+ STATUS MANAGEMENT
+ *******************************************************************************/
+ getStatusText(status) {
+ let map = {
'received': 'Image Received',
'local_processing': 'Processing Image...',
'queued': 'Waiting to upload...',
@@ -108,2510 +1080,10 @@
'failed_permanent': 'Upload failed permanently'
};
- this.init();
- }
-
- async init() {
- // this.initializeFields();
- this.initListeners();
-
- // Queue integration - handle completion/failure
- this.queue.subscribe((event, operation) => {
- if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
- return;
- }
-
- const fieldId = operation.data instanceof FormData
- ? operation.data.get('fieldId')
- : operation.data?.fieldId;
-
- switch(event) {
- case 'cancel-operation':
- if (fieldId) this.handleOperationCancelled(fieldId);
- break;
- case 'operation-status':
- if (fieldId) this.updateFieldStatus(fieldId, operation.status);
- break;
- case 'operation-complete':
- this.handleOperationComplete(operation, fieldId);
- break;
- case 'operation-failed':
- case 'operation-failed-permanent':
- this.handleOperationFailed(operation, fieldId);
- break;
- }
- });
-
- window.addEventListener('beforeunload', () => {
- this.cleanupAllPreviewUrls();
- });
- }
-
- initWorker() {
- this.worker = {
- worker: null,
- timeout: null,
- tasks: new Map(),
- restart: { count: 0, max: 3 },
- settings: {
- timeout: 10000,
- batchSize: 1,
- maxConcurrent: 3,
- restartAfterTimeout: true
- }
- };
- }
-
- /*******************************************************************************
- * FIELD MANAGEMENT
- *******************************************************************************/
- scanFields(container, autoUpload) {
- console.log(autoUpload, 'autoUpload');
- const fields = container.querySelectorAll(this.selectors.field.field);
- fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
- }
-
- registerUploader(uploader, autoUpload) {
- const fieldId = this.determineFieldId(uploader);
- const config = this.extractFieldConfig(uploader, autoUpload);
- const ui = this.buildFieldUI(uploader);
-
- console.log(config, 'registering with config');
- // Store field data with Sets for runtime
- const fieldData = {
- id: fieldId,
- config: config,
- uploads: new Set(),
- groups: [],
- state: 'ready',
- timestamp: Date.now()
- };
-
- // Save to store (will convert Sets to Arrays automatically)
- this.fieldStore.save(fieldData);
-
- // Store DOM references separately
- this.fieldElements.set(fieldId, { element: uploader, ui, config });
-
- uploader.dataset.uploader = fieldId;
- this.addFieldSelectionHandler(fieldId);
-
- if (config.type !== 'single') {
- this.initSortable(fieldId);
- }
-
- return fieldId;
- }
-
- extractFieldConfig(fieldElement, autoUpload) {
- return {
- autoUpload: autoUpload,
- destination: fieldElement.dataset.destination || 'meta',
- content: fieldElement.dataset.content || null,
- mode: fieldElement.dataset.mode || 'direct',
- type: fieldElement.dataset.type || 'single',
- name: fieldElement.dataset.field,
- itemID: fieldElement.dataset.itemId || 0,
- maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
- subtype: fieldElement.dataset.subtype || 'image'
- };
- }
-
- buildFieldUI(fieldElement) {
- let UI = {
- field: fieldElement,
- input: fieldElement.querySelector(this.selectors.field.input),
- dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
- preview: fieldElement.querySelector(this.selectors.field.preview),
- progress: {
- progress: fieldElement.querySelector(this.selectors.field.progress),
- bar: fieldElement.querySelector('.bar'),
- fill: fieldElement.querySelector('.fill'),
- details: fieldElement.querySelector('.details'),
- text: fieldElement.querySelector('.details .text'),
- count: fieldElement.querySelector('.details .count')
- }
- };
-
- let display = fieldElement.querySelector('.group-display');
- if (display) {
- UI.groups = {
- display: display,
- container: fieldElement.querySelector('.item-grid.groups'),
- empty: fieldElement.querySelector('.empty-group'),
- groups: new Map()
- };
- }
-
- return UI;
- }
-
- /*******************************************************************************
- * SORTABLE INITIALIZATION
- *******************************************************************************/
- initSortable(fieldId) {
- if (!window.Sortable) return;
-
- // Mount MultiDrag plugin once
- if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
- Sortable.mount(new Sortable.MultiDrag());
- Sortable._multiDragMounted = true;
- }
-
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldEl) return;
-
- // Initialize sortable on all existing grids
- const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
- grids.forEach(grid => {
- const groupId = grid.classList.contains('group')
- ? grid.closest('.upload-group')?.dataset.groupId
- : null;
- this.createSortableForGrid(grid, fieldId, groupId);
- });
-
- // Special handler for empty-group
- const emptyGroup = fieldEl.element.querySelector('.empty-group');
- if (emptyGroup && !emptyGroup.sortableInstance) {
- emptyGroup.sortableInstance = new Sortable(emptyGroup, {
- animation: 150,
- draggable: '.item',
- multiDrag: true,
- selectedClass: 'selected-for-drag',
- avoidImplicitDeselect: true,
- group: { name: fieldId, pull: false, put: true },
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- dragClass: 'sortable-drag',
- onEnd: (evt) => this.handleDrop(evt, fieldId)
- });
- }
- }
-
- syncSortableSelection(fieldId, selectedItems) {
- // Update Sortable's selection state to match checkboxes
- this.sortableInstances.forEach((instance, key) => {
- if (key.startsWith(fieldId)) {
- const grid = instance.el;
- const items = grid.querySelectorAll('.item');
-
- items.forEach(item => {
- const uploadId = item.dataset.uploadId;
- const shouldBeSelected = selectedItems.has(uploadId);
-
- if (shouldBeSelected) {
- Sortable.utils.select(item);
- } else {
- Sortable.utils.deselect(item);
- }
- });
- }
- });
- }
-
- handleDrop(evt, fieldId) {
- const dropTarget = evt.to;
- const sourceTarget = evt.from;
- const items = evt.items?.length > 0 ? evt.items : [evt.item];
- const uploadIds = items.map(item => item.dataset.uploadId);
-
- // Determine drop target type
- const targetType = this.getDropTargetType(dropTarget);
-
- switch (targetType) {
- case 'empty-group':
- this.handleDropToEmptyGroup(items, uploadIds, fieldId);
- break;
-
- case 'preview':
- this.handleDropToPreview(items, uploadIds, fieldId);
- break;
-
- case 'group':
- this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
- break;
- default:
- // Fallback: return to preview
- this.handleDropToPreview(items, uploadIds, fieldId);
- break;
- }
-
- // Update UI
- this.updateSortableState(dropTarget);
- if (sourceTarget !== dropTarget) {
- this.updateSortableState(sourceTarget);
- }
- }
-
- /**
- * Determine what type of drop target this is
- */
- getDropTargetType(target) {
- if (target.classList.contains('empty-group')) {
- return 'empty-group';
- }
-
- if (target.classList.contains('preview')) {
- return 'preview';
- }
-
- if (target.classList.contains('group')) {
- return 'group';
- }
-
- return 'unknown';
- }
-
- /**
- * Handle drop to group: add to existing group
- */
- handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
- try {
- // If same container, it's just a reorder
- if (dropTarget === sourceTarget) {
- this.handleReorder({ to: dropTarget, items: items });
- return;
- }
-
- // Moving to different group
- uploadIds.forEach(uploadId => {
- this.addToGroup(uploadId, dropTarget, false);
- });
-
- this.schedulePersistance(fieldId);
-
- const message = items.length > 1
- ? `Moved ${items.length} items to group`
- : 'Moved item to group';
- this.a11y.announce(message);
-
- // Clear selection
- const handler = this.selectionHandlers.get(fieldId);
- handler?.clearSelection();
- } catch (error) {
- this.handleDropError(items, fieldId, error);
- }
- }
-
- /**
- * Handle drop to preview: remove from groups
- */
- handleDropToPreview(items, uploadIds, fieldId) {
- try {
- uploadIds.forEach(uploadId => {
- this.removeFromGroup(uploadId);
- });
-
- this.schedulePersistance(fieldId);
-
- const message = items.length > 1
- ? `Moved ${items.length} items to preview`
- : 'Moved item to preview';
- this.a11y.announce(message);
-
- // Clear selection
- const handler = this.selectionHandlers.get(fieldId);
- handler?.clearSelection();
- } catch (error) {
- this.handleDropError(items, fieldId, error);
- }
- }
-
- /**
- * Handle drop errors consistently
- */
- handleDropError(items, fieldId, error, message = 'An error occurred') {
- console.error('Drop error:', error);
-
- // Return items to preview as fallback
- const fieldEl = this.fieldElements.get(fieldId);
- if (fieldEl?.ui?.preview) {
- items.forEach(item => fieldEl.ui.preview.appendChild(item));
- }
-
- this.a11y.announce(`${message}. Items returned to preview.`);
- }
-
- /**
- * Handle drop to group: add to existing group
- */
- handleDropToEmptyGroup(items, uploadIds, fieldId) {
- try {
- const group = this.createGroup(fieldId);
- if (!group) {
- this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
- return;
- }
-
- // Move items to new group
- items.forEach((item, index) => {
- group.grid.appendChild(item);
- this.addToGroup(uploadIds[index], group.grid, false);
- });
-
- this.schedulePersistance(fieldId);
-
- const message = items.length > 1
- ? `Created group with ${items.length} items`
- : 'Created group with item';
- this.a11y.announce(message);
-
- // Clear selection after move
- const handler = this.selectionHandlers.get(fieldId);
- handler?.clearSelection();
- } catch (error) {
- this.handleDropError(items, fieldId, error);
- }
- }
-
- /**
- * Update sortable enabled/disabled state based on item count
- */
- updateSortableState(grid) {
- const sortable = grid?.sortableInstance;
- if (!sortable) return;
-
- // const hasItems = grid.querySelectorAll('.item').length > 0;
- sortable.option('disabled', false);
- }
-
- /**
- * Refresh sortable for a field (call after adding/removing items dynamically)
- */
- refreshSortable(fieldId) {
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldEl) return;
-
- const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
- grids.forEach(grid => this.updateSortableState(grid));
- }
-
- handleReorder(evt) {
- const grid = evt.to;
- const fieldWrapper = grid.closest('.field, .upload');
- if (!fieldWrapper) return;
-
- // Get current order from DOM
- let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
- .map(upload => upload.dataset.uploadId)
- .filter(id => id);
-
-
- // Update hidden input (for form submission)
- let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
- if (hiddenInput && items.length > 0) {
- hiddenInput.value = items.join(',');
- }
-
- // Update fieldState with new order
- const fieldId = this.getFieldIdFromElement(grid);
- if (fieldId) {
- const fieldData = this.getFieldData(fieldId);
-
- // If reordering within a group, update that group's uploads array
- if (grid.classList.contains('group')) {
- const groupId = grid.dataset.groupId;
- const group = fieldData?.groups?.find(g => g.id === groupId);
- if (group) {
- group.uploads = items; // Update order
- }
- }
- // If reordering in preview, the order is implicit by DOM position
- // (we don't store preview order separately)
-
- this.schedulePersistance(fieldId);
- }
-
- this.a11y.announce('Item reordered');
-
- fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
- detail: {
- from: evt.from,
- to: evt.to,
- oldIndex: evt.oldIndex,
- newIndex: evt.newIndex,
- items: items
- },
- bubbles: true
- }));
- }
-
- /*******************************************************************************
- * FILE DROP HANDLERS
- *******************************************************************************/
-
- initListeners() {
- this.clickHandler = this.handleClick.bind(this);
- this.changeHandler = this.handleChange.bind(this);
-
- document.addEventListener('click', this.clickHandler);
- document.addEventListener('change', this.changeHandler);
-
- this.dragEnterHandler = this.handleExternalDragEnter.bind(this);
- this.dragLeaveHandler = this.handleExternalDragLeave.bind(this);
- this.dragOverHandler = this.handleExternalDragOver.bind(this);
- this.dropHandler = this.handleExternalDrop.bind(this);
-
- document.addEventListener('dragenter', this.dragEnterHandler);
- document.addEventListener('dragleave', this.dragLeaveHandler);
- document.addEventListener('dragover', this.dragOverHandler);
- document.addEventListener('drop', this.dropHandler);
- }
-
- handleExternalDragLeave(e) {
- const dropZone = e.target.closest(this.selectors.field.dropZone);
- if (dropZone && !dropZone.contains(e.relatedTarget)) {
- dropZone.classList.remove('dragover');
- }
- }
-
- handleExternalDragEnter(e) {
- if (!e.dataTransfer.types.includes('Files')) return;
- const dropZone = e.target.closest(this.selectors.field.dropZone);
- if (dropZone) {
- e.preventDefault();
- dropZone.classList.add('dragover');
- }
- }
-
- handleExternalDragOver(e) {
- if (!e.dataTransfer.types.includes('Files')) return;
- const dropZone = e.target.closest(this.selectors.field.dropZone);
- if (dropZone) {
- e.preventDefault();
- e.dataTransfer.dropEffect = 'copy';
- }
- }
-
- handleExternalDrop(e) {
- const dropZone = e.target.closest(this.selectors.field.dropZone);
- if (!dropZone) return;
-
- e.preventDefault();
- dropZone.classList.remove('dragover');
-
- const files = Array.from(e.dataTransfer.files);
- if (files.length === 0) return;
-
- const fieldId = this.getFieldIdFromElement(dropZone);
- if (fieldId) {
- this.processFiles(fieldId, files);
- this.a11y.announce(`${files.length} file(s) dropped for upload`);
- }
- }
-
- /*******************************************************************************
- * CLICK & CHANGE HANDLERS
- *******************************************************************************/
-
- handleClick(e) {
- // Trigger file input
- if (e.target.matches(this.selectors.field.dropZone) ||
- e.target.closest(this.selectors.field.dropZone)) {
- const dropZone = e.target.closest(this.selectors.field.dropZone);
- if (dropZone && !e.target.matches('input, button, a')) {
- const input = dropZone.querySelector(this.selectors.field.input);
- input?.click();
- }
- }
-
- // Group actions
- const actionButton = e.target.closest('[data-action]');
- if (actionButton) {
- this.handleAction(actionButton);
- }
- }
-
- handleChange(e) {
- const fieldId = this.getFieldIdFromElement(e.target);
-
- // File input change
- if (e.target.matches(this.selectors.field.input)) {
- const files = Array.from(e.target.files);
- if (files.length > 0 && fieldId) {
- this.processFiles(fieldId, files);
- }
- }
-
- // Meta field changes
- if (fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData.config.autoUpload) {
- return;
- }
- if (fieldData?.config.destination === 'post_group') {
- this.handleGroupMetaChange(e.target);
- } else {
- this.queueUploadMeta(e);
- }
- }
- }
-
- /*******************************************************************************
- * FILE PROCESSING
- *******************************************************************************/
-
- async processFiles(fieldId, files) {
- const fieldData = this.getFieldData(fieldId);
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldData || !fieldEl) return;
-
- // Show group display, hide upload zone
- if (fieldEl.ui.dropZone) {
- fieldEl.ui.dropZone.hidden = true;
- }
- if (fieldEl.ui.groups?.display) {
- fieldEl.ui.groups.display.hidden = false;
- }
-
- const totalFiles = files.length;
- let processedCount = 0;
-
- this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-
- const processPromises = Array.from(files).map(async (file) => {
- try {
- const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
- // Create initial upload data
- const uploadData = {
- id: uploadId,
- attachmentId: null,
- fieldId: fieldId,
- status: 'local_processing',
- groupId: null,
- meta: {
- originalName: file.name,
- size: file.size,
- type: file.type
- }
- };
-
- // Save initial data
- await this.uploadStore.save(uploadData);
-
- // Process file
- const preview = this.createPreviewUrl(file);
- const processedFile = file.type.startsWith('image/')
- ? await this.processImage(file, fieldData.config.subtype)
- : file;
-
- // Show progress
- this.showUploadProgress(uploadId, true);
- this.updateUploadItemProgress(uploadId, 50, 'local_processing');
-
- // Store blob data (this updates the existing uploadData)
- await this.saveBlobData(uploadId, processedFile || file);
-
- // Create DOM element
- const subtype = this.getSubtypeFromMime(file.type);
- const element = this.createUploadElement({
- id: uploadId,
- preview: preview,
- meta: uploadData.meta,
- subtype: subtype
- }, fieldData.config.destination === 'post_group');
-
- // Add to preview grid
- if (fieldEl.ui.preview) {
- fieldEl.ui.preview.appendChild(element);
-
- // Store runtime element data
- this.uploadElements.set(uploadId, {
- element: element,
- preview: preview,
- location: fieldEl.ui.preview
- });
- }
-
- // Update status (gets existing data with blobData intact)
- const storedUpload = this.uploadStore.get(uploadId);
- if (storedUpload) {
- storedUpload.status = 'processed';
- await this.uploadStore.save(storedUpload);
- }
-
- // Add to field
- fieldData.uploads.add(uploadId);
- await this.saveFieldData(fieldData);
-
- // Update progress
- processedCount++;
- this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
- this.updateUploadItemProgress(uploadId, 100, 'processed');
-
- // Fade out progress
- setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
-
- return uploadId;
-
- } catch (error) {
- console.error('Error processing file:', file.name, error);
- processedCount++;
- this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
- return null;
- }
- });
-
- await Promise.all(processPromises);
-
- this.updateFieldState(fieldId);
- this.refreshSortable(fieldId);
-
- // Queue for upload if in direct mode
- if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
- await this.queueUpload(fieldId);
- this.maybeLockUploads(fieldId);
- }
- }
-
- /*******************************************************************************
- * IMAGE PROCESSING
- *******************************************************************************/
-
- async processImage(file, uploadId) {
- const timeout = this.worker.settings.timeout;
-
- return new Promise((resolve, reject) => {
- let timeoutId;
- let taskCompleted = false;
-
- timeoutId = setTimeout(() => {
- if (!taskCompleted) {
- taskCompleted = true;
- this.worker.tasks.delete(uploadId);
- if (this.worker.settings.restartAfterTimeout) {
- this.restartCompressionWorker();
- }
- reject(new Error(`Processing timeout for ${file.name}`));
- }
- }, timeout);
-
- this.worker.tasks.set(uploadId, { file, timeoutId });
-
- this.handleProcess(file, uploadId)
- .then(result => {
- if (!taskCompleted) {
- taskCompleted = true;
- clearTimeout(timeoutId);
- this.worker.tasks.delete(uploadId);
- resolve(result);
- }
- })
- .catch(error => {
- if (!taskCompleted) {
- taskCompleted = true;
- clearTimeout(timeoutId);
- this.worker.tasks.delete(uploadId);
- reject(error);
- }
- });
- });
- }
-
- async handleProcess(file, uploadId) {
- if (!file.type.startsWith('image/')) {
- return file;
- }
-
- const maxDimension = this.getMaxDimension();
- const quality = 0.85;
-
- if (this.shouldUseWorker(file)) {
- try {
- if (!this.worker.worker) {
- this.initCompressionWorker();
- }
- if (this.worker.worker) {
- return await this.processWithWorker(file, uploadId, maxDimension, quality);
- }
- } catch (error) {
- console.warn('Worker processing failed, falling back to main thread:', error);
- }
- }
-
- return await this.processOnMainThread(file, maxDimension, quality);
- }
-
- async processOnMainThread(file, maxDimension, quality) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- let objectUrl = null;
-
- const cleanup = () => {
- img.onload = null;
- img.onerror = null;
- if (objectUrl) {
- URL.revokeObjectURL(objectUrl);
- objectUrl = null;
- }
- canvas.width = 1;
- canvas.height = 1;
- ctx.clearRect(0, 0, 1, 1);
- };
-
- img.onload = () => {
- try {
- const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
- canvas.width = width;
- canvas.height = height;
-
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
- ctx.drawImage(img, 0, 0, width, height);
-
- const outputFormat = this.getOptimalFormat(file);
- const outputQuality = this.getOptimalQuality(file, quality);
-
- canvas.toBlob(
- (blob) => {
- cleanup();
- if (blob) {
- const processedFile = new File(
- [blob],
- this.getProcessedFileName(file, outputFormat),
- { type: outputFormat, lastModified: Date.now() }
- );
- resolve(processedFile);
- } else {
- reject(new Error('Canvas toBlob failed'));
- }
- },
- outputFormat,
- outputQuality
- );
-
- } catch (error) {
- cleanup();
- reject(new Error(`Canvas processing failed: ${error.message}`));
- }
- };
-
- img.onerror = () => {
- cleanup();
- reject(new Error(`Failed to load image: ${file.name}`));
- };
-
- try {
- objectUrl = this.createPreviewUrl(file);
- img.src = objectUrl;
- } catch (error) {
- cleanup();
- reject(new Error(`Failed to create object URL: ${error.message}`));
- }
- });
- }
-
- getOptimalFormat(file) {
- if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
- return file.type;
- }
- return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
- }
-
- getOptimalQuality(file, requestedQuality) {
- if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
- if (file.size < 2 * 1024 * 1024) return requestedQuality;
- return Math.min(requestedQuality, 0.8);
- }
-
- getProcessedFileName(originalFile, outputFormat) {
- const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
- const extensions = {
- 'image/webp': '.webp',
- 'image/jpeg': '.jpg',
- 'image/png': '.png',
- 'image/gif': '.gif'
- };
- return baseName + (extensions[outputFormat] || '.jpg');
- }
-
- getMaxDimension() {
- const screenWidth = window.screen.width;
- const devicePixelRatio = window.devicePixelRatio || 1;
- if (screenWidth * devicePixelRatio > 2560) return 2400;
- if (screenWidth * devicePixelRatio > 1920) return 1920;
- return 1200;
- }
-
- shouldUseWorker(file) {
- return this.worker.worker &&
- file.size > 1024 * 1024 &&
- typeof OffscreenCanvas !== 'undefined';
- }
-
- async processWithWorker(file, uploadId, maxDimension, quality) {
- return new Promise((resolve, reject) => {
- if (!this.worker.worker) {
- reject(new Error('Worker not available'));
- return;
- }
-
- const messageId = `${uploadId}_${Date.now()}`;
-
- const messageHandler = (e) => {
- if (e.data.messageId !== messageId) return;
-
- this.worker.worker.removeEventListener('message', messageHandler);
- this.worker.worker.removeEventListener('error', errorHandler);
-
- if (e.data.success) {
- const processedFile = new File(
- [e.data.blob],
- this.getProcessedFileName(file, e.data.format || 'image/webp'),
- { type: e.data.format || 'image/webp', lastModified: Date.now() }
- );
- resolve(processedFile);
- } else {
- reject(new Error(e.data.error || 'Worker processing failed'));
- }
- };
-
- const errorHandler = (error) => {
- this.worker.worker.removeEventListener('message', messageHandler);
- this.worker.worker.removeEventListener('error', errorHandler);
- reject(new Error(`Worker error: ${error.message}`));
- };
-
- this.worker.worker.addEventListener('message', messageHandler);
- this.worker.worker.addEventListener('error', errorHandler);
-
- this.worker.worker.postMessage({
- messageId,
- file,
- maxDimension,
- quality,
- outputFormat: this.getOptimalFormat(file)
- });
- });
- }
-
- restartCompressionWorker() {
- if (this.worker.worker) {
- this.worker.worker.terminate();
- this.worker.worker = null;
- }
- this.worker.tasks.clear();
- if (this.worker.restart.count >= this.worker.restart.max) {
- console.error('Max worker restarts reached, disabling worker');
- return;
- }
- this.worker.restart.count++;
- this.initCompressionWorker();
- }
-
- initCompressionWorker() {
- if (this.worker.worker || typeof Worker === 'undefined') return;
-
- try {
- const workerScript = `
- self.onmessage = async function(e) {
- const { messageId, file, maxDimension, quality, outputFormat } = e.data;
- try {
- const bitmap = await createImageBitmap(file);
- const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
- const width = Math.round(bitmap.width * scale);
- const height = Math.round(bitmap.height * scale);
- const canvas = new OffscreenCanvas(width, height);
- const ctx = canvas.getContext('2d');
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
- ctx.drawImage(bitmap, 0, 0, width, height);
- bitmap.close();
- const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });
- self.postMessage({ messageId, success: true, blob: blob, format: outputFormat });
- } catch (error) {
- self.postMessage({ messageId, success: false, error: error.message });
- }
- };
- `;
-
- const blob = new Blob([workerScript], { type: 'application/javascript' });
- this.worker.worker = new Worker(this.createPreviewUrl(blob));
-
- } catch (error) {
- console.warn('Failed to initialize compression worker:', error);
- this.worker.worker = null;
- }
- }
-
- calculateOptimalDimensions(img, maxDimension) {
- let { width, height } = img;
- if (width <= maxDimension && height <= maxDimension) {
- return { width, height };
- }
- const scale = Math.min(maxDimension / width, maxDimension / height);
- return {
- width: Math.round(width * scale),
- height: Math.round(height * scale)
- };
- }
-
- supportsWebP() {
- const canvas = document.createElement('canvas');
- return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
- }
-
- createPreviewUrl(file) {
- const url = URL.createObjectURL(file);
- if (!this.previewUrls) this.previewUrls = new Set();
- this.previewUrls.add(url);
- return url;
- }
-
- revokePreviewUrl(url) {
- if (url?.startsWith('blob:')) {
- URL.revokeObjectURL(url);
- this.previewUrls?.delete(url);
- }
- }
-
- /*******************************************************************************
- * QUEUE INTEGRATION
- *******************************************************************************/
-
- async submitUploads(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldData?.uploads || fieldData.uploads.size === 0) {
- return;
- }
-
- let uploadIds = Array.from(fieldData.uploads);
- if (uploadIds.length === 0) {
- this.error.log('No uploads to upload', {
- component: 'UploadManager',
- action: 'submitGroupedUploads',
- fieldId: fieldId
- });
- return;
- }
-
- const fieldGroups = this.getFieldGroups(fieldId);
-
- if (fieldGroups.length === 0) {
- this.error.log('No groups created for post_group upload', {
- component: 'UploadManager',
- action: 'submitGroupedUploads',
- fieldId: fieldId
- });
- return;
- }
-
- // Build posts array from groups
- const posts = [];
- const formData = new FormData();
- let uploadMap = [];
-
- // Process each group
- for (const group of fieldGroups) {
- const post = {
- images: [],
- fields: {}
- };
-
- // Add group metadata
- for (let [name, value] of Object.entries(group.changes)) {
- post.fields[name] = value;
- }
-
- // Get uploads for this group
- const groupUploadIds = uploadIds.filter(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- return upload?.groupId === group.id;
- });
-
- // Add files for this group
- for (const uploadId of groupUploadIds) {
- const file = await this.getBlobData(uploadId);
- if (file) {
- formData.append('files[]', file);
-
- const imageData = {
- upload_id: uploadId,
- index: uploadMap.length
- };
-
- // Check if featured
- const uploadEl = this.uploadElements.get(uploadId);
- const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
- if (radioInput?.checked) {
- post.fields.featured = uploadId;
- }
-
- post.images.push(imageData);
- uploadMap.push(uploadId);
- }
- }
-
- posts.push(post);
- }
-
- // Handle remaining uploads (without groupId) - each becomes its own post
- const remainingUploadIds = uploadIds.filter(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- return !upload?.groupId;
- });
-
- for (const uploadId of remainingUploadIds) {
- const post = {
- images: [],
- fields: {}
- };
-
- const file = await this.getBlobData(uploadId);
- if (file) {
- formData.append('files[]', file);
-
- const imageData = {
- upload_id: uploadId,
- index: uploadMap.length
- };
- post.images.push(imageData);
- uploadMap.push(uploadId);
- }
-
- posts.push(post);
- }
-
- // Add metadata to FormData
- formData.append('content', fieldData.config.content);
- formData.append('user', fieldData.config.itemID);
- formData.append('posts', JSON.stringify(posts));
- formData.append('upload_ids', JSON.stringify(uploadMap));
-
- const operation = {
- endpoint: 'uploads/groups',
- method: 'POST',
- data: formData,
- title: `Creating ${posts.length} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
- popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
- canMerge: false,
- headers: {
- 'action_nonce': window.auth.getNonce('dash')
- },
- append: '_upload',
- };
-
- try {
- const operationId = await this.queue.addToQueue(operation);
-
- // Update upload statuses
- uploadIds.forEach(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- if (upload) {
- upload.operationId = operationId;
- upload.status = 'queued';
- this.uploadStore.save(upload);
- this.updateUploadStatus(uploadId, 'queued');
- }
- });
-
- fieldData.operationId = operationId;
- await this.saveFieldData(fieldData);
-
- this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
-
- return operationId;
- } catch (error) {
- this.error.log(error, {
- component: 'UploadManager',
- action: 'submitGroupedUploads',
- fieldId: fieldId
- });
- throw error;
- }
- }
-
- async queueUpload(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
-
- const uploads = Array.from(fieldData.uploads);
- const data = this.prepareUploadData(fieldData, uploads);
-
- this.a11y.announce('Queuing for upload');
-
- const operation = {
- endpoint: 'uploads',
- method: 'POST',
- data: data,
- title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
- popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
- canMerge: false,
- headers: { 'action_nonce': window.auth.getNonce('dash') },
- append: '_upload'
- };
-
- try {
- const operationId = await this.queue.addToQueue(operation);
-
- // Update upload statuses
- uploads.forEach(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- if (upload) {
- upload.operationId = operationId;
- upload.status = 'queued';
- this.uploadStore.save(upload);
- this.updateUploadStatus(uploadId, 'queued');
- }
- });
-
- fieldData.operationId = operationId;
- await this.saveFieldData(fieldData);
-
- return operationId;
- } catch (error) {
- throw error;
- }
- }
-
- async prepareUploadData(fieldData, uploads) {
- const formData = new FormData();
- formData.append('content', fieldData.config.content);
- formData.append('mode', fieldData.config.mode);
- formData.append('field_name', fieldData.config.name);
- formData.append('fieldId', fieldData.id);
- formData.append('field_type', fieldData.config.type);
- formData.append('subtype', fieldData.config.subtype);
- formData.append('item_id', fieldData.config.itemID);
- formData.append('destination', fieldData.config.destination || 'meta');
-
- let uploadMap = [];
-
-
- const blobPromises = uploads.map(async (uploadId) => {
- const upload = this.uploadStore.get(uploadId);
- if (!upload) return;
-
- const file = await this.getBlobData(uploadId);
- if (file) {
- formData.append('files[]', file);
- uploadMap.push(upload.id);
- }
- });
-
- await Promise.all(blobPromises);
-
- formData.append('upload_ids', JSON.stringify(uploadMap));
- return formData;
- }
-
- async queueUploadMeta(e) {
- const uploadId = this.getUploadIdFromElement(e.target);
- const upload = this.uploadStore.get(uploadId);
- if (!upload) return;
-
- const fieldData = this.getFieldData(upload.fieldId);
- if (!fieldData) return;
-
- let data = {};
- data[e.target.name] = e.target.value;
-
- upload.meta = { ...upload.meta, ...data };
- await this.uploadStore.save(upload);
-
- let queueData = {};
- queueData[upload.attachmentId ?? upload.id] = upload.meta;
-
- const operation = {
- endpoint: 'uploads/meta',
- method: 'POST',
- data: queueData,
- title: 'Updating meta',
- canMerge: true,
- headers: { 'action_nonce': window.auth.getNonce('dash') }
- };
-
- try {
- await this.queue.addToQueue(operation);
- } catch (error) {
- this.error.log(error, {
- component: 'UploadManager',
- action: 'sendMetaUpdate',
- uploadId: upload.id
- });
- }
- }
-
- /*******************************************************************************
- * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
- *******************************************************************************/
-
- /**
- * Handle successful operation completion - CLEAR STORES
- */
- async handleOperationComplete(operation, fieldId) {
- const results = operation.result?.data || operation.serverData?.data || [];
-
- // Update upload statuses with attachment IDs
- results.forEach(result => {
- const upload = this.uploadStore.get(result.upload_id);
- if (upload) {
- upload.attachmentId = result.attachment_id;
- upload.status = 'completed';
- this.uploadStore.save(upload);
- this.updateUploadStatus(result.upload_id, 'completed');
- }
- });
-
- if (!fieldId) return;
-
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData) return;
-
- // Clean up completed uploads from stores
- const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- return upload?.status === 'completed';
- });
-
- for (const uploadId of completedUploads) {
- await this.clearUpload(uploadId, false);
- fieldData.uploads.delete(uploadId);
- }
-
- // If all uploads complete, clear entire field from stores
- if (fieldData.uploads.size === 0) {
- await this.clearFieldFromStores(fieldId);
- this.a11y.announce('All uploads completed successfully');
- } else {
- // Otherwise just update field state
- await this.saveFieldData(fieldData);
- }
-
- this.updateFieldState(fieldId);
- }
-
- /**
- * Handle operation failure
- */
- handleOperationFailed(operation, fieldId) {
- const uploadIds = operation.data instanceof FormData
- ? JSON.parse(operation.data.get('upload_ids') || '[]')
- : operation.data.upload_ids || [];
-
- uploadIds.forEach(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- if (upload) {
- upload.status = operation.status === 'operation-failed-permanent'
- ? 'failed_permanent'
- : 'failed';
- this.uploadStore.save(upload);
- this.updateUploadStatus(uploadId, upload.status);
- }
- });
-
- if (fieldId) {
- this.updateFieldState(fieldId);
- }
- }
-
- /**
- * Handle operation cancellation
- */
- async handleOperationCancelled(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData) return;
-
- const uploadsArray = fieldData.uploads instanceof Set
- ? Array.from(fieldData.uploads)
- : fieldData.uploads;
-
- for (const uploadId of uploadsArray) {
- await this.clearUpload(uploadId, false);
- }
-
- await this.clearFieldFromStores(fieldId);
- this.updateFieldState(fieldId);
- this.a11y.announce('Upload cancelled');
- }
-
- getFieldGroups(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData?.groups) return [];
-
- return fieldData.groups.map(group => ({
- id: group.id,
- uploads: group.uploads || [],
- changes: group.changes || {}
- }));
- }
-
- getSelectedRestorationUploads(notificationEl) {
- let selected = [];
- const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
-
- checkboxes.forEach(checkbox => {
- const item = checkbox.closest('.item');
- if (item) {
- selected.push({
- uploadId: item.dataset.uploadId,
- fieldId: item.dataset.fieldId
- });
- }
- });
-
- return selected;
- }
-
- async restoreSelectedUploads(selectedUploads) {
- const byField = new Map();
- selectedUploads.forEach(item => {
- if (!byField.has(item.fieldId)) {
- byField.set(item.fieldId, []);
- }
- byField.get(item.fieldId).push(item.uploadId);
- });
-
- for (const [fieldId, uploadIds] of byField.entries()) {
- const fieldState = this.fieldStore.get(fieldId);
- if (fieldState) {
- fieldState.uploads = uploadIds;
- await this.restoreField(fieldState);
- }
- }
- }
-
- async restoreField(fieldState) {
- const { config, context, uploads, groups, id } = fieldState;
-
- // If in a modal, open it first
- if (context?.modalType) {
- await this.openModalForRestore(context);
- }
-
- // Find field element
- let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
-
- if (!fieldElement) {
- const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
- fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
- }
-
- if (!fieldElement) {
- console.warn(`Field ${config.name} not found for restoration`, config);
- return;
- }
-
- // Register the field if not already registered
- let fieldKey = fieldElement.dataset.uploader;
- if (!fieldKey || !this.fieldElements.has(fieldKey)) {
- fieldKey = this.registerUploader(fieldElement);
- }
-
- const fieldEl = this.fieldElements.get(fieldKey);
- const fieldData = this.getFieldData(fieldKey);
-
- if (!fieldEl || !fieldData) {
- console.error('Failed to register field for restoration');
- return;
- }
-
- // Merge saved state back into field
- fieldData.state = fieldState.state || 'ready';
-
- // Rebuild UI references if needed
- if (!fieldEl.ui) {
- fieldEl.ui = this.buildFieldUI(fieldElement);
- }
-
- if (fieldEl.ui.groups?.display) {
- fieldEl.ui.groups.display.hidden = false;
- }
- if (fieldEl.ui.dropZone) {
- fieldEl.ui.dropZone.hidden = true;
- }
-
- // Restore groups first
- if (groups && groups.length > 0) {
- await this.restoreGroups(fieldKey, groups);
- }
-
- // Handle both Array and Set for uploads
- const uploadsArray = uploads instanceof Set
- ? Array.from(uploads)
- : Array.isArray(uploads)
- ? uploads
- : [];
-
- // Restore uploads
- for (const uploadId of uploadsArray) {
- // Get upload data from store
- const uploadData = this.uploadStore.get(uploadId);
- if (uploadData) {
- await this.restoreUpload(fieldKey, uploadData);
- }
- }
-
- // Update field state
- await this.saveFieldData(fieldData);
- this.updateFieldState(fieldKey);
- this.maybeLockUploads(fieldKey);
- this.refreshSortable(fieldKey);
-
- // Queue for upload if needed
- console.log(config);
- if (config.autoUpload && config.mode === 'direct' && config.destination !== 'post_group') {
- await this.queueUpload(fieldKey);
- }
- }
-
- async restoreUpload(fieldId, uploadData) {
- const fieldEl = this.fieldElements.get(fieldId);
- const fieldData = this.getFieldData(fieldId);
-
- if (!fieldEl || !fieldData) {
- console.error('Field not found for upload restoration:', fieldId);
- return;
- }
-
- // Get reconstructed File from blob data
- const file = await this.getBlobData(uploadData.id);
-
- if (!file) {
- console.warn('Blob data not found for upload:', uploadData.id);
- return;
- }
-
- // Create preview URL
- const previewUrl = this.createPreviewUrl(file);
-
- // Recreate DOM element
- const subtype = this.getSubtypeFromMime(file.type);
- const element = this.createUploadElement({
- id: uploadData.id,
- preview: previewUrl,
- meta: uploadData.meta || {
- originalName: file.name,
- size: file.size,
- type: file.type
- },
- subtype: subtype
- }, fieldData.config.destination === 'post_group');
-
- // Determine correct location
- let location;
- if (uploadData.groupId) {
- // Check if group exists
- const groupEl = this.groupElements.get(uploadData.groupId);
- if (groupEl?.grid) {
- location = groupEl.grid;
-
- // Add to group's upload list
- const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
- if (group) {
- if (!group.uploads) group.uploads = [];
- if (!group.uploads.includes(uploadData.id)) {
- group.uploads.push(uploadData.id);
- }
- }
- } else {
- // Group doesn't exist, add to preview
- location = fieldEl.ui.preview;
- uploadData.groupId = null;
- }
- } else {
- // No group, add to preview
- location = fieldEl.ui.preview;
- }
-
- // Add element to DOM
- if (location) {
- location.appendChild(element);
- } else if (fieldEl.ui.preview) {
- fieldEl.ui.preview.appendChild(element);
- location = fieldEl.ui.preview;
- }
-
- // Store runtime element data
- this.uploadElements.set(uploadData.id, {
- element: element,
- preview: previewUrl,
- location: location
- });
-
- // Add to field uploads
- if (!fieldData.uploads) fieldData.uploads = new Set();
- fieldData.uploads.add(uploadData.id);
-
- // Update upload data in store
- uploadData.status = 'processed';
- await this.uploadStore.save(uploadData);
-
- // Update sortable state for the grid
- if (location) {
- this.updateSortableState(location);
- }
- }
-
- async restoreGroups(fieldId, groups) {
- const fieldEl = this.fieldElements.get(fieldId);
- const fieldData = this.getFieldData(fieldId);
-
- if (!fieldEl || !fieldData) {
- console.error('Field not found for group restoration:', fieldId);
- return;
- }
-
- for (const groupData of groups) {
- const group = this.createGroup(fieldId, groupData.id);
- if (!group) {
- console.warn('Failed to create group:', groupData.id);
- continue;
- }
-
- const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
- if (storedGroup) {
- // Restore metadata
- if (groupData.changes) {
- storedGroup.changes = { ...groupData.changes };
- }
-
- // Preserve upload order
- if (groupData.uploads) {
- storedGroup.uploads = [...groupData.uploads];
- }
-
- // Restore form field values
- if (groupData.changes) {
- const titleInput = group.element.querySelector('[name*="post_title"]');
- const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
-
- if (titleInput && groupData.changes.post_title) {
- titleInput.value = groupData.changes.post_title;
- }
- if (excerptInput && groupData.changes.post_excerpt) {
- excerptInput.value = groupData.changes.post_excerpt;
- }
- }
- }
- }
-
- await this.saveFieldData(fieldData);
- }
-
- async openModalForRestore(context) {
- if (!context) return;
-
- const { modalType, itemId } = context;
-
- // Find and click the appropriate button to open the modal
- let trigger = null;
-
- switch(modalType) {
- case 'create':
- trigger = document.querySelector('[data-action="create"]');
- break;
- case 'edit':
- // Need to find the specific edit button
- if (itemId) {
- trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
- }
- break;
- case 'bulkEdit':
- trigger = document.querySelector('[data-action="bulk-edit"]');
- break;
- }
-
- if (trigger) {
- trigger.click();
-
- // Wait for modal to open and render
- await new Promise(resolve => setTimeout(resolve, 300));
- } else {
- console.warn('Modal trigger not found for restoration:', context);
- }
- }
-
- formatBytes(bytes, decimals = 2) {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const dm = decimals < 0 ? 0 : decimals;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
- }
-
- /*******************************************************************************
- * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
- *******************************************************************************/
-
- /**
- * Clear individual upload from stores (called after successful upload)
- */
- async clearUpload(uploadId, persist = true) {
- const uploadEl = this.uploadElements.get(uploadId);
- if (uploadEl) {
- this.revokePreviewUrl(uploadEl.preview);
- if (uploadEl.element) {
- const previewUrl = uploadEl.element.dataset.previewUrl;
- this.revokePreviewUrl(previewUrl);
- delete uploadEl.element.dataset.previewUrl;
- }
- }
-
- // Remove from runtime memory
- this.uploadElements.delete(uploadId);
-
- // Remove from store (no separate blob store - it's part of the upload object)
- await this.uploadStore.delete(uploadId);
-
- // Update field if needed
- if (persist) {
- const upload = this.uploadStore.get(uploadId);
- if (upload?.fieldId) {
- await this.schedulePersistance(upload.fieldId);
- }
- }
- }
-
- /**
- * Clear entire field from stores (called when all uploads complete)
- */
- async clearFieldFromStores(fieldId) {
- const fieldData = this.getFieldData(fieldId);
-
- // Clear all related uploads
- if (fieldData?.uploads) {
- const uploadsArray = fieldData.uploads instanceof Set
- ? Array.from(fieldData.uploads)
- : fieldData.uploads;
-
- for (const uploadId of uploadsArray) {
- await this.uploadStore.delete(uploadId);
- }
- }
-
- // Clear field from store
- await this.fieldStore.delete(fieldId);
-
- // Keep runtime references (fieldElements, etc) intact for reuse
- }
-
- cleanupAllPreviewUrls() {
- if (this.previewUrls) {
- this.previewUrls.forEach(url => {
- try {
- URL.revokeObjectURL(url);
- } catch (e) {
- // Ignore errors during cleanup
- }
- });
- this.previewUrls.clear();
- }
- }
-
- /*******************************************************************************
- * UI UPDATE METHODS
- *******************************************************************************/
-
- updateFieldState(fieldId) {
- const fieldEl = this.fieldElements.get(fieldId);
- const fieldData = this.getFieldData(fieldId);
- if (!fieldEl || !fieldData) return;
-
- const container = fieldEl.element;
- const uploadCount = fieldData.uploads?.size || 0;
- const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
- container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
- container.dataset.uploadCount = uploadCount.toString();
- container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
- if (fieldEl.ui.preview) {
- fieldEl.ui.preview.setAttribute('aria-label',
- `Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
- );
- }
- }
-
- updateUploadProgress(fieldId, current, total, message) {
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldEl?.ui?.progress?.progress) return;
-
- const progress = fieldEl.ui.progress;
- const percent = total > 0 ? (current / total) * 100 : 0;
-
- if (progress.fill) progress.fill.style.width = `${percent}%`;
- if (progress.text) progress.text.textContent = message;
- if (progress.count) progress.count.textContent = `${current}/${total}`;
-
- progress.progress.hidden = (current === total);
- }
-
- updateFieldStatus(fieldId, status) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData) return;
-
- fieldData.state = status;
- this.saveFieldData(fieldData);
- }
-
- updateUploadStatus(uploadId, status) {
- const upload = this.uploadStore.get(uploadId);
- if (!upload) return;
-
- upload.status = status;
- this.uploadStore.save(upload);
- this.updateUploadUI(uploadId);
- }
-
- updateUploadUI(uploadId) {
- const uploadEl = this.uploadElements.get(uploadId);
- const upload = this.uploadStore.get(uploadId);
- if (!upload || !uploadEl?.element) return;
-
- uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
- uploadEl.element.classList.add(`status-${upload.status}`);
-
- const progress = uploadEl.element.querySelector('.progress');
- if (progress) {
- this.updateUploadItemProgress(uploadId,
- this.getStatusProgress(upload.status),
- upload.status
- );
- }
- }
-
- showUploadProgress(uploadId, show = true) {
- const uploadEl = this.uploadElements.get(uploadId);
- if (!uploadEl?.element) return;
-
- const progressEl = uploadEl.element.querySelector('.progress');
- if (progressEl) {
- if (show) {
- progressEl.style.removeProperty('animation');
- progressEl.hidden = false;
- } else {
- progressEl.style.animation = 'fadeOut var(--transition-base)';
- setTimeout(() => { progressEl.hidden = true; }, 300);
- }
- }
- }
-
- updateUploadItemProgress(uploadId, percent, status = null) {
- const uploadEl = this.uploadElements.get(uploadId);
- if (!uploadEl?.element) return;
-
- const progressEl = uploadEl.element.querySelector('.progress');
- if (!progressEl) return;
-
- const fill = progressEl.querySelector('.fill');
- const details = progressEl.querySelector('.details');
- const icon = progressEl.querySelector('.icon');
-
- if (fill) fill.style.width = `${percent}%`;
- if (status && details) details.textContent = this.getStatusText(status);
- if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
- }
-
- maybeLockUploads(fieldId) {
- const fieldEl = this.fieldElements.get(fieldId);
- const fieldData = this.getFieldData(fieldId);
- if (!fieldEl?.ui?.dropZone || !fieldData) return;
-
- const uploadCount = fieldData.uploads?.size || 0;
-
- // For groupable uploads, set max to 20
- const maxFiles = fieldData.config.destination === 'post_group'
- ? 20
- : (fieldData.config?.maxFiles || 999);
-
- fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
- fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
-
- // Show helpful message for groupable uploads
- if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
- this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
- }
- }
-
- /*******************************************************************************
- * GROUP MANAGEMENT
- *******************************************************************************/
- /**
- * Create sortable instance for a grid
- */
- createSortableForGrid(grid, fieldId, groupId = null) {
- if (!grid || grid.sortableInstance) return;
-
- const sortableInstance = new Sortable(grid, {
- animation: 150,
- draggable: '.item',
- multiDrag: true,
- selectedClass: 'selected-for-drag',
- avoidImplicitDeselect: true,
- group: { name: fieldId, pull: true, put: true },
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- dragClass: 'sortable-drag',
-
- // Centralized drop handler
- onEnd: (evt) => this.handleDrop(evt, fieldId),
-
- // Selection sync
- onSelect: (evt) => {
- const checkbox = evt.item.querySelector('[name*="select-item"]');
- if (checkbox && !checkbox.checked) {
- checkbox.checked = true;
- checkbox.dispatchEvent(new Event('change', { bubbles: true }));
- }
- },
-
- onDeselect: (evt) => {
- const checkbox = evt.item.querySelector('[name*="select-item"]');
- if (checkbox && checkbox.checked) {
- checkbox.checked = false;
- checkbox.dispatchEvent(new Event('change', { bubbles: true }));
- }
- },
-
- onAdd: (evt) => this.updateSortableState(evt.to),
- onRemove: (evt) => this.updateSortableState(evt.from)
- });
-
- grid.sortableInstance = sortableInstance;
-
- const gridId = groupId
- ? `${fieldId}-group-${groupId}`
- : `${fieldId}-preview`;
-
- this.sortableInstances.set(gridId, sortableInstance);
-
- return sortableInstance;
- }
- createGroup(fieldId, groupId = null) {
- const fieldData = this.getFieldData(fieldId);
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldData || !fieldEl) return null;
-
- if (!groupId) {
- groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- }
-
- const groupElement = this.createGroupElement(groupId, fieldId);
- if (!groupElement) return null;
-
- // Store in field UI Map
- if (!fieldEl.ui.groups) {
- fieldEl.ui.groups = {
- groups: new Map(),
- container: null,
- empty: null,
- display: null
- };
- }
-
- fieldEl.ui.groups.groups.set(groupId, groupElement);
-
- // Insert into DOM
- if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
- fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
- } else if (fieldEl.ui.groups.container) {
- fieldEl.ui.groups.container.appendChild(groupElement);
- }
-
- // Store group element reference
- const grid = groupElement.querySelector('.item-grid.group');
- this.groupElements.set(groupId, {
- element: groupElement,
- grid: grid,
- fieldId: fieldId
- });
-
- // Add to field groups
- if (!fieldData.groups) fieldData.groups = [];
- const existingGroup = fieldData.groups.find(g => g.id === groupId);
- if (!existingGroup) {
- fieldData.groups.push({
- id: groupId,
- uploads: [],
- changes: {}
- });
- this.saveFieldData(fieldData);
- }
-
- // Initialize selection handler
- this.addGroupSelectionHandler(fieldId, groupId);
-
- if (grid) {
- this.createSortableForGrid(grid, fieldId, groupId);
- }
-
- return { id: groupId, element: groupElement, grid: grid };
- }
- createGroupElement(groupId, fieldId) {
- let groupElement = window.getTemplate('imageGroup');
- if (!groupElement) return;
-
- groupElement.dataset.groupId = groupId;
- groupElement.dataset.fieldId = fieldId;
-
- let fields = window.getTemplate('groupMetadata');
- const fieldsContainer = groupElement.querySelector('.fields');
- if (fieldsContainer && fields) {
- fieldsContainer.append(fields);
-
- const titleInput = fieldsContainer.querySelector('[name="post_title"]');
- const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
-
- if (titleInput) {
- titleInput.id = `${groupId}_title`;
- titleInput.name = `${groupId}[post_title]`;
- }
- if (excerptInput) {
- excerptInput.id = `${groupId}_excerpt`;
- excerptInput.name = `${groupId}[post_excerpt]`;
- }
-
- const fieldData = this.getFieldData(fieldId);
- if (fieldData && fieldData.config.content !== '') {
- let summary = groupElement.querySelector('summary');
- if (summary) summary.textContent = fieldData.config.content + ' Fields';
- }
- } else {
- groupElement.querySelector('details')?.remove();
- }
-
- const gridContainer = groupElement.querySelector('.item-grid.group');
- if (gridContainer) {
- gridContainer.dataset.groupId = groupId;
- }
-
- return groupElement;
- }
-
- deleteGroup(groupId, confirm = true) {
- const groupEl = this.groupElements.get(groupId);
- if (!groupEl) return;
-
- const fieldData = this.getFieldData(groupEl.fieldId);
- if (!fieldData) return;
-
- const group = fieldData.groups?.find(g => g.id === groupId);
- let keepUploads = true;
-
- if (confirm && group?.uploads?.length > 0) {
- keepUploads = !window.confirm('Delete uploads in group?');
- }
-
- if (confirm && keepUploads && group?.uploads) {
- // Move uploads back to preview
- group.uploads.forEach(uploadId => {
- this.removeFromGroup(uploadId);
- });
- }
-
- // Remove from field groups
- if (fieldData.groups) {
- fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
- this.saveFieldData(fieldData);
- }
-
- // Remove DOM element
- if (groupEl.element) {
- groupEl.element.remove();
- this.a11y.announce('Group removed');
- }
-
- // Remove from maps
- this.groupElements.delete(groupId);
-
- // Clean up sortable
- const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
- const sortable = this.sortableInstances.get(sortableKey);
- if (sortable?.destroy) {
- sortable.destroy();
- }
- this.sortableInstances.delete(sortableKey);
-
- this.schedulePersistance(groupEl.fieldId);
- }
-
- addToGroup(uploadId, target = null, persist = true) {
- const upload = this.uploadStore.get(uploadId);
- const uploadEl = this.uploadElements.get(uploadId);
- if (!upload || !uploadEl) return;
-
- const fieldData = this.getFieldData(upload.fieldId);
- const fieldEl = this.fieldElements.get(upload.fieldId);
- if (!fieldData || !fieldEl) return;
-
- // Already in correct location
- if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
- return;
- }
-
- // Remove from previous group
- if (upload.groupId) {
- const group = fieldData.groups?.find(g => g.id === upload.groupId);
- if (group) {
- group.uploads = group.uploads.filter(id => id !== uploadId);
- if (group.uploads.length === 0) {
- this.deleteGroup(upload.groupId);
- }
- }
- }
-
- // Clear selection checkbox
- const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
- if (checkbox) checkbox.checked = false;
-
- let featured = uploadEl.element.querySelector('[name="featured"]');
- if (featured) featured.hidden = !target;
-
- // Moving to preview or to group
- if (!target || target.classList.contains('preview')) {
- target = fieldEl.ui.preview;
- upload.groupId = null;
- } else {
- // Moving to group
- const groupId = target.dataset.groupId;
- if (featured) featured.name = groupId + '_' + featured.name;
-
- const group = fieldData.groups?.find(g => g.id === groupId);
- if (group) {
- if (!group.uploads) group.uploads = [];
- group.uploads.push(uploadId);
- upload.groupId = groupId;
- }
- }
-
- // Update location
- uploadEl.location = target;
- target.append(uploadEl.element);
-
- // Update stores
- this.uploadStore.save(upload);
- if (persist) {
- this.saveFieldData(fieldData);
- }
-
- // Update sortable state
- this.updateSortableState(target);
- if (uploadEl.location && uploadEl.location !== target) {
- this.updateSortableState(uploadEl.location);
- }
- }
-
- removeFromGroup(uploadId) {
- const upload = this.uploadStore.get(uploadId);
- const uploadEl = this.uploadElements.get(uploadId);
- if (!upload || !uploadEl) return;
-
- const fieldData = this.getFieldData(upload.fieldId);
- const fieldEl = this.fieldElements.get(upload.fieldId);
- if (!fieldData || !fieldEl) return;
-
- // Remove from current group
- if (upload.groupId) {
- const group = fieldData.groups?.find(g => g.id === upload.groupId);
- if (group) {
- group.uploads = group.uploads.filter(id => id !== uploadId);
- if (group.uploads.length === 0) {
- this.deleteGroup(upload.groupId, false);
- }
- }
- upload.groupId = null;
- }
-
- // Move back to preview
- if (fieldEl.ui?.preview) {
- fieldEl.ui.preview.appendChild(uploadEl.element);
- uploadEl.location = fieldEl.ui.preview;
- }
-
- // Hide featured radio
- const featured = uploadEl.element.querySelector('[name="featured"]');
- if (featured) {
- featured.hidden = true;
- featured.checked = false;
- }
-
- this.uploadStore.save(upload);
- this.updateSortableState(fieldEl.ui.preview);
- }
-
- removeUpload(fieldId, uploadId) {
- const fieldData = this.getFieldData(fieldId);
- const upload = this.uploadStore.get(uploadId);
- const uploadEl = this.uploadElements.get(uploadId);
-
- if (!fieldData || !upload) return;
-
- // Remove from field
- fieldData.uploads?.delete(uploadId);
-
- // Remove from group if grouped
- if (upload.groupId) {
- const group = fieldData.groups?.find(g => g.id === upload.groupId);
- if (group) {
- group.uploads = group.uploads.filter(id => id !== uploadId);
- if (group.uploads.length === 0) {
- this.deleteGroup(upload.groupId);
- }
- }
- }
-
- // Clean up element
- uploadEl?.element?.remove();
-
- // Clean up memory
- this.clearUpload(uploadId);
-
- // Update field state
- this.saveFieldData(fieldData);
- this.updateFieldState(fieldId);
- this.maybeLockUploads(fieldId);
-
- const handler = this.selectionHandlers.get(fieldId);
- if (handler) {
- handler.deselect(uploadId);
- }
-
- this.a11y.announce('Upload removed');
- }
-
- handleGroupMetaChange(input) {
- const groupEl = this.getGroupFromElement(input);
- if (!groupEl) return;
-
- const fieldData = this.getFieldData(groupEl.fieldId);
- const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
- if (!group) return;
-
- if (!group.changes) group.changes = {};
-
- let name = input.name;
- if (name.includes('group')) {
- name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
- }
-
- group.changes[name] = input.value;
- this.saveFieldData(fieldData);
- this.schedulePersistance(groupEl.fieldId);
- }
-
- /*******************************************************************************
- * ACTION HANDLERS
- *******************************************************************************/
-
- handleAction(button) {
- const action = button.dataset.action;
- const fieldId = this.getFieldIdFromElement(button);
-
- switch(action) {
- case 'add-to-group':
- this.handleAddToGroup(button);
- break;
- case 'delete-group':
- this.handleDeleteGroup(button);
- break;
- case 'delete-upload':
- case 'remove-from-group':
- this.handleRemoveItem(button);
- break;
- case 'upload':
- const fieldEl = this.fieldElements.get(fieldId);
- if (fieldEl) {
- fieldEl.element.closest('details').open = false;
- document.body.classList.add('uploading');
- this.submitUploads(fieldId);
- }
- break;
- case 'restore':
- this.handleRestoreUploads().then(() => {});
- break;
- case 'restore-all':
- this.handleRestoreAll().then(() => {});
- break;
- case 'clear-cache':
- if (!confirm('Save these uploads for later?')) {
- this.cleanupStoredUploads();
- }
- this.cleanupRestore();
- break;
- }
- }
-
- handleAddToGroup(button) {
- const fieldElement = button.closest(this.selectors.field.field);
- const fieldId = fieldElement?.dataset.uploader;
- if (!fieldId) return;
-
- const selected = this.selected.get(fieldId);
-
- if (!selected || selected.size === 0) {
- this.createGroup(fieldId);
- } else {
- const group = this.createGroup(fieldId);
- if (!group) return;
-
- selected.forEach(uploadId => {
- this.addToGroup(uploadId, group.grid);
- });
-
- const handler = this.selectionHandlers.get(fieldId);
- handler?.clearSelection();
-
- this.a11y.announce(`Created group with ${selected.size} items`);
- }
-
- this.schedulePersistance(fieldId);
- }
-
- handleDeleteGroup(button) {
- const group = button.closest(this.selectors.groups.container);
- if (!group) return;
-
- const groupId = group.dataset.groupId;
- const fieldId = this.getFieldIdFromElement(group);
-
- if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
- return;
- }
-
- const items = group.querySelectorAll(this.selectors.items.item);
- items.forEach(item => {
- const uploadId = item.dataset.uploadId;
- this.removeFromGroup(uploadId);
- });
-
- this.deleteGroup(groupId);
- this.a11y.announce('Group deleted, items returned to upload area');
- this.schedulePersistance(fieldId);
- }
-
- handleRemoveItem(button) {
- const item = button.closest(this.selectors.items.item);
- if (!item) return;
-
- const uploadId = item.dataset.uploadId;
- const fieldId = this.getFieldIdFromElement(item);
-
- if (!confirm('Remove this item?')) return;
-
- this.removeUpload(fieldId, uploadId);
- this.a11y.announce('Item removed');
- this.schedulePersistance(fieldId);
- }
-
- /*******************************************************************************
- * SELECTION MANAGEMENT
- *******************************************************************************/
-
- addFieldSelectionHandler(fieldId) {
- if (this.selectionHandlers.has(fieldId)) {
- return this.selectionHandlers.get(fieldId);
- }
-
- const fieldEl = this.fieldElements.get(fieldId);
- if (!fieldEl?.element) return;
-
- const handler = new window.jvbHandleSelection({
- container: fieldEl.element,
- ui: {
- selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
- bulkControls: fieldEl.element.querySelector('.selection-actions'),
- count: fieldEl.element.querySelector('.selection-count')
- },
- itemSelector: '[data-upload-id]',
- checkboxSelector: '[name*="select-item"]'
- });
-
- handler.subscribe((event, data) => {
- switch(event) {
- case 'item-selected':
- // Sync with Sortable
- this.syncSortableSelection(fieldId, data.selectedItems);
- this.selected.set(fieldId, data.selectedItems);
- break;
- case 'item-deselected':
- this.syncSortableSelection(fieldId, data.selectedItems);
- this.selected.set(fieldId, data.selectedItems);
- break;
- case 'range-selected':
- this.syncSortableSelection(fieldId, data.selectedItems);
- this.selected.set(fieldId, data.selectedItems);
- break;
- case 'select-all':
- this.handleSelectAll(data.container, data.selected);
- break;
- }
- });
-
- this.selectionHandlers.set(fieldId, handler);
- return handler;
- }
-
- addGroupSelectionHandler(fieldId, groupId) {
- const handlerKey = `${fieldId}_${groupId}`;
- if (this.selectionHandlers.has(handlerKey)) {
- return this.selectionHandlers.get(handlerKey);
- }
-
- const groupEl = this.groupElements.get(groupId);
- if (!groupEl?.element) return;
-
- const handler = new window.jvbHandleSelection({
- container: groupEl.element,
- ui: {
- selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
- bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
- count: groupEl.element.querySelector(this.selectors.groups.count)
- },
- itemSelector: '[data-upload-id]',
- checkboxSelector: '[name*="select-item"]'
- });
-
- handler.subscribe((event, data) => {
- switch(event) {
- case 'item-selected':
- case 'item-deselected':
- case 'range-selected':
- this.selected.set(fieldId, data.selectedItems);
- break;
- case 'select-all':
- this.handleSelectAll(data.container, data.selected);
- break;
- }
- });
-
- this.selectionHandlers.set(handlerKey, handler);
- return handler;
- }
-
- handleSelectAll(container, selected) {
- // Can add custom logic here if needed
- }
-
- /*******************************************************************************
- * HELPER METHODS
- *******************************************************************************/
-
- /**
- * Get field data from store and normalize it
- * Always use this instead of directly accessing fieldStore.get()
- */
- getFieldData(fieldId) {
- const fieldData = this.fieldStore.get(fieldId);
- if (!fieldData) return null;
-
- // Only convert uploads back to Set (DataStore returns Arrays)
- if (Array.isArray(fieldData.uploads)) {
- fieldData.uploads = new Set(fieldData.uploads);
- } else if (!fieldData.uploads) {
- fieldData.uploads = new Set();
- }
-
- // Ensure groups is an array
- if (!Array.isArray(fieldData.groups)) {
- fieldData.groups = [];
- }
-
- return fieldData;
- }
-
- /**
- * Save field data to store, converting Sets to Arrays
- */
- async saveFieldData(fieldData) {
- await this.fieldStore.save({
- ...fieldData,
- timestamp: Date.now()
- });
- }
-
- determineFieldId(fieldElement) {
- const content = fieldElement.dataset.content ||
- fieldElement.closest('dialog')?.dataset.content ||
- fieldElement.closest('form')?.dataset.save || '';
- const itemID = fieldElement.dataset.itemId ||
- fieldElement.closest('dialog')?.dataset.itemId || '';
- const field = fieldElement.dataset.field || '';
-
- return `${content}_${itemID}_${field}`;
- }
-
- getFromElement(element, type) {
- const map = {
- 'field': {
- selector: this.selectors.field.field,
- key: 'uploader',
- getRuntimeData: (id) => this.fieldElements.get(id),
- getStoreData: (id) => this.getFieldData(id)
- },
- 'upload': {
- selector: this.selectors.items.item,
- key: 'uploadId',
- getRuntimeData: (id) => this.uploadElements.get(id),
- getStoreData: (id) => this.uploadStore.get(id)
- },
- 'group': {
- selector: this.selectors.groups.container,
- key: 'groupId',
- getRuntimeData: (id) => this.groupElements.get(id),
- getStoreData: (id) => {
- // Groups are stored in field.groups array
- const groupEl = this.groupElements.get(id);
- if (!groupEl) return null;
- const fieldData = this.getFieldData(groupEl.fieldId);
- return fieldData?.groups?.find(g => g.id === id);
- }
- }
- };
-
- const config = map[type];
- if (!config) return null;
-
- const el = element.closest(config.selector);
- if (!el) return null;
-
- const id = el.dataset[config.key];
-
- // Return combined runtime + store data for convenience
- const runtime = config.getRuntimeData(id);
- const store = config.getStoreData(id);
-
- return { ...runtime, ...store };
- }
-
- getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
- getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
- getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
-
- getFieldIdFromElement(el) {
- const field = this.getFromElement(el, 'field');
- return field?.id ?? null;
- }
- getUploadIdFromElement(el) {
- const upload = this.getFromElement(el, 'upload');
- return upload?.id ?? null;
- }
- getGroupIdFromElement(el) {
- const group = this.getFromElement(el, 'group');
- return group?.id ?? null;
- }
-
- getSubtypeFromMime(mimeType) {
- if (mimeType.startsWith('image/')) return 'image';
- if (mimeType.startsWith('video/')) return 'video';
- return 'document';
- }
-
- getStatusText(status) {
- return this.statusMapping[status] || status;
- }
-
- getStatusIcon(status) {
- return window.getIcon(this.queue.icons[status]);
+ return map[status]||status;
}
-
getStatusProgress(status) {
- const progress = {
+ let progress = {
'local_processing': 28,
'queued': 50,
'uploading': 66,
@@ -2619,16 +1091,21 @@
'processing': 89,
'completed': 100
};
- return progress[status] || 0;
+ return progress[status]??0;
}
-
-
- createUploadElement(upload, draggable = false) {
+ /*******************************************************************************
+ UPLOAD METHODS
+ *******************************************************************************/
+ async createUpload(uploadId, file, fieldId) {
let image = window.getTemplate('uploadItem');
- if (!image) return;
+ if (!image) return null;
- image.dataset.uploadId = upload.id;
- image.dataset.subtype = upload.subtype || 'image';
+ let field = this.fields.get(fieldId);
+ if (!field) return null;
+
+ image.dataset.uploadId = uploadId;
+ let mimeType = this.getSubtypeFromMime(file.type)||'image';
+ image.dataset.subtype = mimeType;
let [featured, img, video, preview, details] = [
image.querySelector('[name="featured"]'),
@@ -2638,33 +1115,37 @@
image.querySelector('details')
];
- if (featured) featured.value = upload.id;
-
- switch (upload.subtype) {
+ if (featured) featured.value = uploadId;
+ switch (mimeType) {
case 'image':
if (img) {
- img.src = upload.preview;
- img.alt = upload.meta?.originalName || '';
+ const previewUrl = this.createPreviewUrl(file);
+ img.src = previewUrl;
+ img.alt = file.name || '';
+ img.dataset.previewUrl = previewUrl;
}
video?.remove();
preview?.remove();
break;
case 'video':
- if (video) video.src = upload.preview;
+ if (video){
+ const previewUrl = this.createPreviewUrl(file);
+ video.src = previewUrl;
+ video.dataset.previewUrl = previewUrl;
+ }
img?.remove();
preview?.remove();
break;
case 'document':
- const fileName = upload.meta?.originalName || '';
- const extension = fileName.split('.').pop()?.toLowerCase() || '';
- const iconMap = {
+ let ext = file.name.split('.').pop()?.toLowerCase()??'';
+ let map = {
'pdf': 'file-pdf', 'csv': 'file-csv',
'doc': 'file-doc', 'docx': 'file-doc',
'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
};
- const icon = window.getIcon(iconMap[extension] || 'file');
+ let icon = window.getIcon(map[ext]??'file');
if (preview) {
- preview.innerText = fileName;
+ preview.innerText = file.name;
preview.prepend(icon);
}
img?.remove();
@@ -2677,13 +1158,12 @@
if (template) details.append(template);
}
- image.draggable = draggable;
+ image.draggable = field.config.type !== 'single'??false;
- // Update input IDs
- image.querySelectorAll('input').forEach(input => {
+ image.querySelectorAll('input').forEach(input => {
let id = input.id;
if (id) {
- let newId = id + upload.id;
+ let newId = id + uploadId;
let label = input.parentNode.querySelector(`label[for="${id}"]`);
input.id = newId;
if (label) label.htmlFor = newId;
@@ -2693,391 +1173,527 @@
return image;
}
- /*******************************************************************************
- * PERSISTENCE
- *******************************************************************************/
+ getSubtypeFromMime(mimeType) {
+ if (mimeType.startsWith('image/')) return 'image';
+ if (mimeType.startsWith('video/')) return 'video';
+ return 'document';
+ }
+ /**
+ * Called by handleAction
+ * @param button
+ */
+ async handleRemoveItem(button) {
+ const item = button.closest(this.selectors.items.item);
+ if (!item) return;
- schedulePersistance(fieldId) {
- const key = `persist_${fieldId}`;
- window.debouncer.schedule(
- key,
- () => this.persistFieldState(fieldId),
- 250
+ const uploadId = item.dataset.uploadId;
+ if (!confirm('Remove this item?')) return;
+ await this.removeUpload(uploadId);
+ this.a11y.announce('Item removed');
+ }
+
+ async setBulkUpload(uploads, key, value) {
+ const promises = Array.from(uploads).map(async (upload) => {
+ if (key === 'status') {
+ await this.setUploadStatus(upload, value);
+ }
+ upload[key] = value;
+ return this.stores.uploads.save(upload);
+ });
+ await Promise.all(promises);
+ }
+
+ async setUploadStatus(upload, status) {
+ if (upload.progress) {
+ window.showProgress(upload.progress, this.getStatusProgress(status), 100, this.getStatusText(status), this.queue.icons[status]??'');
+ }
+ }
+
+ async removeUpload(uploadId) {
+ let upload = this.stores.uploads.get(uploadId);
+ if (!upload) return;
+ if (upload.group) {
+ let group = this.stores.groups.get(upload.group);
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ await this.removeGroup(group.id, false);
+ }
+ }
+
+ await this.clearUpload(uploadId);
+ this.maybeLockUploads(upload.field);
+
+ let handler = this.selectionHandlers.get(upload.field);
+ if (handler){
+ handler.deselect(uploadId);
+ }
+
+ this.a11y.announce('Upload removed');
+ }
+
+ async clearUpload(uploadId) {
+ const element = this.uploads.get(uploadId);
+ if (element) {
+ this.revokePreviewUrl(element.preview);
+ if (element.element) {
+ const previewUrl = element.element.dataset.previewUrl;
+ this.revokePreviewUrl(previewUrl);
+ element.element.remove();
+ }
+ }
+ this.uploads.delete(uploadId);
+ await this.stores.uploads.delete(uploadId);
+ }
+
+ /*******************************************************************************
+ GROUP METHODS
+ *******************************************************************************/
+ async handleAddToGroup(fieldId) {
+ const selected = this.selected.get(fieldId);
+ if (!selected || selected.size === 0) return;
+
+ let groupId = await this.createGroup(fieldId);
+ if (!groupId) return;
+
+ await Promise.all(
+ Array.from(selected).map(uploadId => this.addToGroup(uploadId, groupId))
+ );
+
+ this.selectionHandlers.get(fieldId)?.clearSelection();
+ this.a11y.announce(`Created group with ${selected.size} items`);
+ }
+ async createGroup(fieldId, groupId = null) {
+ let field = this.fields.get(fieldId);
+ if (!field) return;
+
+ if (!groupId) {
+ groupId = window.generateID('group');
+ }
+
+ const element = this.createGroupElement(groupId, fieldId);
+ if (!element) return null;
+
+ field.groupUI.grid.append(element);
+
+ // Create Sortable for this group's grid
+ const grid = element.querySelector('.item-grid');
+ if (grid) {
+ grid.dataset.groupId = groupId;
+ this.createSortableForGrid(fieldId, grid, groupId);
+ }
+
+ let storedData = this.stores.groups.data.has(groupId)
+ ? this.stores.groups.data.get(groupId)
+ : {};
+
+ await this.setGroup(groupId, { ...storedData, id: groupId, field: fieldId });
+
+ return groupId;
+ }
+
+ createGroupElement(groupId, fieldId = null) {
+ let element = window.getTemplate('imageGroup');
+ if (!element) return;
+
+ element.dataset.groupId = groupId;
+ if (fieldId) {
+ element.dataset.fieldId = fieldId;
+ }
+
+ const selectAll = element.querySelector('[data-select-all]');
+ if (selectAll) {
+ const newId = `select-all-${groupId}`;
+ const label = element.querySelector(`label[for="${selectAll.id}"]`);
+ selectAll.id = newId;
+ selectAll.name = newId;
+ if (label) label.htmlFor = newId;
+ }
+
+ let fields = window.getTemplate('groupMetadata');
+ let container = element.querySelector('.fields');
+ if (fields && container) {
+ container.append(fields);
+
+ let title = container.querySelector('[name="post_title"]');
+ let excerpt = container.querySelector('[name="post_excerpt"]');
+
+ if (title) {
+ title.id = `${groupId}_title`;
+ title.name = `${groupId}[post_title]`;
+ }
+ if (excerpt) {
+ excerpt.id = `${groupId}_excerpt`;
+ excerpt.name = `${groupId}[post_excerpt]`;
+ }
+ } else {
+ element.querySelector('details')?.remove();
+ }
+
+ const grid = element.querySelector('.item-grid');
+ if (grid) {
+ grid.dataset.groupId = groupId;
+ }
+
+ this.groups.set(groupId, {
+ element: element,
+ ui: window.uiFromSelectors(this.selectors.group, element)
+ });
+ return element;
+ }
+
+
+ async setGroup(groupId, data) {
+ const defaults = {
+ id: groupId,
+ src: window.location.href,
+ uploads: [],
+ operationId: null,
+ field: null,
+ fields: {}
+ };
+ const group = {...defaults, ...data};
+ Object.preventExtensions(group);
+
+ await this.stores.groups.save(group);
+ }
+
+ async setBulkGroup(fieldId, key, value) {
+ let groups = this.stores.groups.filterByIndex({field:fieldId});
+ if (groups.length === 0) {
+ return;
+ }
+ let Promises = groups.map(group => {
+ group[key] = value;
+ this.stores.groups.save(group);
+ });
+ await Promise.all(Promises);
+ }
+
+ async addToGroup(uploadId, groupId = null){
+ const upload = this.stores.uploads.get(uploadId);
+ const element = this.uploads.get(uploadId);
+ if (!upload || !element) return;
+ const field = this.fields.get(upload.field);
+ if (!field) return;
+
+ //Check if it's already in this destination, it's probably a reorder
+ const isInDOM = element.element?.parentElement !== null;
+ if (isInDOM && ((!groupId && upload.group === null) || groupId === upload.group)) {
+ this.handleReorder(upload.field, groupId);
+ return;
+ }
+
+ if (upload.group) {
+ const group = this.stores.groups.get(upload.group);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ await this.removeGroup(group.id, false);
+ }
+ }
+ }
+
+ //clear any selection
+ if (element.ui.checkbox) element.ui.checkbox.checked = false;
+ if (this.selected.get(upload.field)?.has(uploadId)) {
+ this.selected.get(upload.field).delete(uploadId);
+ }
+ if (element.ui.featured) element.ui.featured.hidden = !groupId;
+
+ if (!groupId) {
+ upload.group = null;
+ } else {
+ if (element.ui.featured) element.ui.featured.name = `${groupId}_featured`;
+ let group = this.stores.groups.get(groupId);
+ if (group) {
+ group.uploads.push(uploadId);
+ upload.group = groupId;
+ this.stores.groups.save(group);
+ }
+ }
+
+ let target = (groupId) ? this.groups.get(groupId)?.ui.grid : field.ui.grid;
+ if (target) {
+ target.append(element.element)
+ }
+ this.stores.uploads.save(upload);
+ }
+
+ handleDeleteGroup(button) {
+ const group = button.closest(this.selectors.group.item);
+ if (!group) return;
+
+ let groupId = group.dataset.groupId;
+ if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+ return;
+ }
+
+ let uploads = this.stores.uploads.filterByIndex({group: groupId});
+
+ Promise.all(
+ uploads.map(upload => this.addToGroup(upload.id, null))
+ ).then(() => {
+ this.removeGroup(groupId, false).then(()=>{});
+ this.a11y.announce('Group deleted. Items returned to upload area');
+ });
+ }
+
+ async removeGroup(groupId, confirm = true) {
+ let element = this.groups.get(groupId);
+ let group = this.stores.groups.get(groupId);
+ if (!group) return;
+
+ let keepUploads = true;
+
+ if (confirm && group.uploads.length > 0) {
+ keepUploads = window.confirm('Keep uploads in this group?');
+ }
+
+ await Promise.all(
+ group.uploads.map(uploadId =>
+ keepUploads ? this.addToGroup(uploadId, null) : this.removeUpload(uploadId)
+ )
+ );
+
+ // Destroy the Sortable for this group
+ const sortableKey = this.getGroupKey(group.field, groupId);
+ const sortable = this.sortables.get(sortableKey);
+ if (sortable?.destroy) {
+ sortable.destroy();
+ }
+ this.sortables.delete(sortableKey);
+
+ if (element?.element) {
+ element.element.remove();
+ }
+ this.groups.delete(groupId);
+
+ await this.stores.groups.delete(groupId);
+
+ this.a11y.announce('Group removed');
+ }
+
+ maybeLockUploads(fieldId) {
+ let field = this.fields.get(fieldId);
+ if (!field || !field.ui.dropZone) return;
+
+ let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ let count = uploads.length;
+ let max = field.config.maxFiles??25;
+
+ field.ui.dropZone.hidden = count >= max;
+ }
+ /*******************************************************************************
+ OPERATION METHODS
+ *******************************************************************************/
+ async handleOperationCancelled(fieldId) {
+ const uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ const groups = this.stores.groups.filterByIndex({field: fieldId});
+
+ await Promise.all([
+ ...uploads.map(upload => this.removeUpload(upload.id)),
+ ...groups.map(group => this.removeGroup(group.id, false))
+ ]);
+ this.a11y.announce('Upload Cancelled');
+ }
+
+ async handleOperationFailed(operation, fieldId) {
+ // Mark uploads as failed, maybe show retry UI
+ await this.setBulkUpload(
+ this.stores.uploads.filterByIndex({field: fieldId}),
+ 'status',
+ 'failed'
);
}
- async persistFieldState(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData) return;
-
- // Save with updated timestamp
- await this.saveFieldData(fieldData);
+ async handleFieldStatus(fieldId, operation) {
+ let status = operation.status;
+ let uploads = this.stores.uploads.filterByIndex({field: fieldId});
+ await this.setBulkUpload(uploads, 'status', status);
}
-
- // In UploadManager, add blob conversion helpers
- async saveBlobData(uploadId, file) {
- const arrayBuffer = await file.arrayBuffer();
-
- const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
-
- // Store blob data as ArrayBuffer with metadata
- uploadData.blobData = {
- buffer: arrayBuffer,
- name: file.name,
- type: file.type,
- size: file.size,
- lastModified: file.lastModified || Date.now()
- };
-
- await this.uploadStore.save(uploadData);
- }
-
- async getBlobData(uploadId) {
- const upload = this.uploadStore.get(uploadId);
- if (!upload?.blobData) return null;
-
- // Reconstruct File from ArrayBuffer
- const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
- return new File([blob], upload.blobData.name, {
- type: upload.blobData.type,
- lastModified: upload.blobData.lastModified
- });
- }
-
/*******************************************************************************
- HELPER to GET UPLOADED FILES
+ SELECTION HANDLERS
*******************************************************************************/
- /**
- * Get all files for a form (searches all upload fields within form)
- */
- async getFilesForForm(formElement) {
- const uploadFields = formElement.querySelectorAll('[data-upload-field]');
- const allFiles = [];
-
- for (const field of uploadFields) {
- const fieldId = this.determineFieldId(field);
- const files = await this.getFilesForField(fieldId);
- allFiles.push(...files);
- }
-
- return allFiles;
+ getGroupKey(fieldId, groupId = null) {
+ return (groupId) ? `${fieldId}_${groupId}` : `${fieldId}`;
}
- /**
- * Get all files for a specific field
- */
- async getFilesForField(fieldId) {
- const fieldData = this.getFieldData(fieldId);
- if (!fieldData?.uploads) return [];
+ getSelectionHandler(fieldId) {
+ let key = this.getGroupKey(fieldId);
- const files = [];
- const uploadsArray = fieldData.uploads instanceof Set
- ? Array.from(fieldData.uploads)
- : fieldData.uploads;
-
- for (const uploadId of uploadsArray) {
- const upload = this.uploadStore.get(uploadId);
- if (!upload) continue;
-
- // Get the actual File object from blob data
- const file = await this.getBlobData(uploadId);
- if (file) {
- files.push({
- file: file,
- uploadId: uploadId,
- fieldName: fieldData.config.name,
- meta: upload.meta || {}
- });
- }
- }
-
- return files;
- }
-
- /*******************************************************************************
- * RECOVERY & RESTORATION
- *******************************************************************************/
-
- handleFieldStoreEvent(event, data) {
- switch(event) {
- case 'data-loaded':
- this.fieldStoreReady = true;
- this.checkIfBothStoresReady();
- break;
- }
- }
-
- handleUploadStoreEvent(event, data) {
- switch(event) {
- case 'data-loaded':
- this.uploadStoreReady = true;
- this.checkIfBothStoresReady();
- break;
- case 'item-saved':
- this.showSaveIndicator(data.key);
- break;
- }
- }
-
- checkIfBothStoresReady() {
- if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
- this.hasCheckedForUploads = true;
- this.checkForStoredUploads();
- }
- }
-
- async checkForStoredUploads() {
- const allFieldStates = this.fieldStore.getAll();
-
- const pendingFields = allFieldStates.filter(field => {
- if (!field.uploads) return false;
-
- // Handle both Set and Array (from IndexedDB)
- const uploadsArray = field.uploads instanceof Set
- ? Array.from(field.uploads)
- : Array.isArray(field.uploads)
- ? field.uploads
- : [];
-
- return uploadsArray.some(uploadId => {
- const upload = this.uploadStore.get(uploadId);
- return upload && !upload.operationId &&
- ['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
+ if (!this.selectionHandlers.has(key)) {
+ let field = this.fields.get(fieldId);
+ if (!field) return;
+ let handler = new window.jvbHandleSelection({
+ container: field.element,
+ item: this.selectors.items.item,
+ count: this.selectors.fields.count,
+ bulkControls: this.selectors.fields.actions,
+ checkbox: this.selectors.items.checkbox,
+ selectAll: this.selectors.fields.selectAll,
+ wrapper: `${this.selectors.fields.preview}, ${this.selectors.group.item}`,
});
- });
- if (pendingFields.length === 0) return;
- await this.showRecoveryNotification(pendingFields);
- }
+ handler.subscribe((event, data) => {
+ this.selected.set(fieldId, data.selectedItems);
+ console.log(Array.from(this.selected));
+ this.syncSortableSelection(fieldId, data.selectedItems);
+ });
- async showRecoveryNotification(pendingFields) {
- const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
- const totalGroups = pendingFields.reduce((sum, field) =>
- sum + (field.groups?.length || 0), 0);
-
- let notification = window.getTemplate('restoreNotification');
- if (!notification) {
- console.error('Restore notification template not found');
- return;
+ this.selectionHandlers.set(key, handler);
}
- // Build appropriate message
- let message;
- if (totalGroups > 0) {
- let group = totalGroups > 1 ? 'groups' : 'group';
- let upload = totalUploads > 1 ? 'uploads' : 'upload';
- message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+ return this.selectionHandlers.get(key);
+ }
+ /*******************************************************************************
+ SORTABLE
+ *******************************************************************************/
+ initSortable(fieldId) {
+ if (!window.Sortable) return;
+
+ const field = this.fields.get(fieldId);
+ if (!field) return;
+
+ if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+ Sortable.mount(new Sortable.MultiDrag());
+ Sortable._multiDragMounted = true;
+ }
+
+ // Create sortable for the main preview grid
+ this.createSortable(fieldId, field.ui.grid, null);
+
+ // Set up empty-group as native drop zone
+ this.initEmptyGroupDropZone(fieldId);
+ }
+
+ createSortable(fieldId, gridElement, groupId) {
+ if (!gridElement) return null;
+
+ const key = this.getGroupKey(fieldId, groupId);
+
+ // Already exists
+ if (this.sortables.has(key)) {
+ return this.sortables.get(key);
+ }
+
+ const sortable = new Sortable(gridElement, {
+ animation: 150,
+ draggable: '.item',
+ multiDrag: true,
+ selectedClass: 'selected',
+ avoidImplicitDeselect: true,
+ group: { name: fieldId, pull: true, put: true },
+ ghostClass: 'ghost',
+ chosenClass: 'chosen',
+ dragClass: 'dragging',
+
+ onStart: () => this.syncSortableSelection(fieldId),
+ onEnd: (evt) => this.sortableDrop(evt, fieldId),
+ });
+
+ this.sortables.set(key, sortable);
+ return sortable;
+ }
+
+ initEmptyGroupDropZone(fieldId) {
+ const field = this.fields.get(fieldId);
+ const emptyZone = field?.groupUI?.empty;
+ if (!emptyZone) return;
+
+ emptyZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ emptyZone.classList.add('drag-over');
+ });
+
+ emptyZone.addEventListener('dragleave', (e) => {
+ if (!emptyZone.contains(e.relatedTarget)) {
+ emptyZone.classList.remove('drag-over');
+ }
+ });
+
+ emptyZone.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ emptyZone.classList.remove('drag-over');
+
+ // Get selected items from our tracking
+ const selectedItems = this.selected.get(fieldId);
+ if (!selectedItems || selectedItems.size === 0) return;
+
+ const groupId = await this.createGroup(fieldId);
+ if (!groupId) return;
+
+ await Promise.all(
+ Array.from(selectedItems).map(uploadId => this.addToGroup(uploadId, groupId))
+ );
+
+ this.selectionHandlers.get(fieldId)?.clearSelection();
+ });
+ }
+
+ async sortableDrop(evt, fieldId) {
+ const dropTarget = evt.to;
+
+ const items = evt.items?.length > 0 ? Array.from(evt.items) : [evt.item];
+ const uploadIds = items.map(item => item.dataset.uploadId).filter(Boolean);
+
+ if (uploadIds.length === 0) return;
+
+ // Determine target group from the grid's data attribute
+ const targetGroupId = dropTarget.dataset.groupId || null;
+
+ await Promise.all(
+ uploadIds.map(uploadId => this.addToGroup(uploadId, targetGroupId))
+ );
+
+ this.selectionHandlers.get(fieldId)?.clearSelection();
+ }
+
+ syncSortableSelection(fieldId) {
+ const selectedItems = this.selected.get(fieldId) || new Set();
+
+ for (const [uploadId, uploadData] of this.uploads) {
+ const upload = this.stores.uploads.get(uploadId);
+ if (!upload || upload.field !== fieldId) continue;
+
+ const element = uploadData.element;
+ if (!element) continue;
+
+ const shouldBeSelected = selectedItems.has(uploadId);
+
+ if (shouldBeSelected && !element.classList.contains('selected')) {
+ Sortable.utils.select(element);
+ } else if (!shouldBeSelected && element.classList.contains('selected')) {
+ Sortable.utils.deselect(element);
+ }
+ }
+ }
+
+ handleReorder(fieldId, groupId = null) {
+ let target = (groupId) ? this.groups.get(groupId)?.ui.grid : this.fields.get(fieldId)?.ui.grid;
+ if (!target) {
+ console.log ('Couldn\'t Reorder items...');
+ return;
+ }
+ //Get current order from DOM
+ let items = Array.from(target.querySelectorAll(this.selectors.items.item+':not(.ghost)'))
+ .map(upload => upload.dataset.uploadId)
+ .filter(id => id);
+
+
+ if (!groupId) {
+ let hiddenInput = this.fields.get(fieldId)?.ui.hidden;
+ if (hiddenInput) {
+ hiddenInput.value = items.join(',');
+ }
} else {
- message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
- }
-
- const detailsEl = notification.querySelector('.restore-details');
- if (detailsEl) {
- detailsEl.textContent = message;
- }
-
- // Build the restoration preview
- for (const field of pendingFields) {
- let fieldTemplate = window.getTemplate('restoreField');
- if (!fieldTemplate) continue;
-
- // Set field name/title
- const titleEl = fieldTemplate.querySelector('h3');
- if (titleEl) {
- titleEl.textContent = field.config.name || 'Unnamed Field';
+ let group = this.groups.get(groupId);
+ if (group) {
+ group.uploads = items;
}
-
- const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
-
- // Process each upload
- for (let uploadId of field.uploads) {
- const upload = this.uploadStore.get(uploadId);
- let uploadItem = window.getTemplate('uploadItem');
- if (!uploadItem) continue;
- //
- // const imgEl = uploadItem.querySelector('img');
- // const placeholderEl = uploadItem.querySelector('.image-placeholder');
- //
- const file = await this.getBlobData(upload.id);
- if (file) {
-
- try {
- // Create new blob URL from stored data
- const previewUrl = this.createPreviewUrl(file);
-
- let [
- featured,
- img,
- video,
- preview,
- details
- ] = [
- uploadItem.querySelector('[name="featured"]'),
- uploadItem.querySelector('img'),
- uploadItem.querySelector('video'),
- uploadItem.querySelector('label > span'),
- uploadItem.querySelector('details')
- ];
-
- uploadItem.dataset.uploadId = upload.id;
-
-
- uploadItem.dataset.fieldId = field.id;
-
- let subtype = this.getSubtypeFromMime(file.type);
- uploadItem.dataset.subtype = subtype;
- switch (subtype) {
- case 'image':
- [
- img.src,
- img.alt
- ] = [
- previewUrl,
- file.name ?? upload.meta?.originalName ?? ''
- ];
- video.remove();
- preview.remove();
- break;
- case 'video':
- video.src = previewUrl;
- img.remove();
- preview.remove();
- break;
- case 'document':
- let extension = '';
- let icon;
- switch (extension) {
- case 'pdf':
- icon = window.getIcon('file-pdf');
- break;
- case 'csv':
- icon = window.getIcon('file-csv');
- break;
- case 'doc':
- icon = window.getIcon('file-doc');
- break;
- case 'txt':
- icon = window.getIcon('file-txt');
- break;
- case 'xls':
- icon = window.getIcon('file-xls');
- break;
- default:
- icon = window.getIcon('file');
- break;
- }
-
- preview.innerText = upload.originalFile.name;
- preview.prepend(icon);
- img.remove();
- video.remove();
- break;
- }
-
- // Store URL for cleanup later
- uploadItem.dataset.previewUrl = previewUrl;
- } catch (error) {
- console.warn('Failed to create preview for upload:', upload.id, error);
- }
- }
-
- // Set upload metadata
- const nameEl = uploadItem.querySelector('summary span');
- if (nameEl) {
- nameEl.textContent = upload.meta?.originalName || 'Unknown file';
- }
-
- const metaEl = uploadItem.querySelector('details');
- if (metaEl && upload.meta) {
- metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
- }
-
- // Update input IDs safely
- uploadItem.querySelectorAll('input').forEach(input => {
- let id = input.id;
- if (id) {
- let newId = id + upload.id;
- let label = input.parentNode.querySelector(`label[for="${id}"]`);
- input.id = newId;
- if (label) {
- label.htmlFor = newId;
- }
- }
- });
-
- if (itemGrid) {
- itemGrid.appendChild(uploadItem);
- }
- }
-
- notification.querySelector('.wrap').appendChild(itemGrid);
}
-
- document.querySelector('.field.upload').appendChild(notification);
- notification = document.querySelector('dialog.restore-uploads');
- this.restoreModal = new window.jvbModal(notification);
- this.restoreSelection = new window.jvbHandleSelection({
- container: notification,
- ui: {
- selectAll: notification.querySelector('#select-all-restore'),
- count: notification.querySelector('.selection-count'),
- },
- });
-
- this.restoreModal.handleOpen();
-
+ this.a11y.announce('Items reordered');
}
-
- async handleRestoreUploads() {
- let notification = document.querySelector('dialog.restore-uploads');
- if (!notification) {
- return;
- }
-
- const selectedUploads = this.getSelectedRestorationUploads(notification);
- if (selectedUploads.length === 0) {
- return;
- }
- await this.restoreSelectedUploads(selectedUploads);
-
- this.cleanupRestore();
- }
-
- async handleRestoreAll() {
- let notification = document.querySelector('dialog.restore-uploads');
- if (!notification) {
- return;
- }
- // Gets ALL uploads from notification without checking selection
- const allUploads = [];
- notification.querySelectorAll('.item.upload').forEach(item => {
- let uploadId = item.dataset.uploadId;
- let fieldId = item.dataset.fieldId;
- allUploads.push({ uploadId, fieldId });
- });
-
- await this.restoreSelectedUploads(allUploads);
- this.cleanupRestore();
- }
-
- showSaveIndicator(key) {
- // Optional: show user that state is being saved
- }
-
- cleanupRestore() {
- this.restoreModal.handleClose();
- this.restoreSelection.destroy();
- this.restoreSelection = null;
- this.restoreModal.destroy();
- this.restoreModal.modal.remove();
- this.restoreModal = null;
- }
-
- async cleanupStoredUploads() {
- await this.fieldStore.clear();
- await this.uploadStore.clear();
- }
-
/*******************************************************************************
* EVENT SYSTEM
*******************************************************************************/
@@ -3089,49 +1705,95 @@
notify(event, data = {}) {
this.subscribers.forEach(cb => {
- try {
- cb(event, data);
- } catch (error) {
- console.error('Subscriber error:', error);
- }
+ try { cb(event, data); } catch (e) { console.error('Subscriber error:', e); }
});
}
-
- /*******************************************************************************
- * DESTROY & CLEANUP
- *******************************************************************************/
-
+ /********************************************************************
+ CLEANUP
+ ********************************************************************/
destroy() {
- document.removeEventListener('click', this.clickHandler);
- document.removeEventListener('change', this.changeHandler);
- document.removeEventListener('dragenter', this.dragEnterHandler);
- document.removeEventListener('dragleave', this.dragLeaveHandler);
- document.removeEventListener('dragover', this.dragOverHandler);
- document.removeEventListener('drop', this.dropHandler);
+ this.subscribers.clear();
+ this.previewUrls.forEach(url => {
+ this.revokePreviewUrl(url);
+ });
+ this.previewUrls.clear();
+ }
- if (this.dragController) {
- this.dragController.destroy();
+ cleanupAllPreviewUrls() {
+ this.previewUrls.forEach(url => this.revokePreviewUrl(url));
+ this.previewUrls.clear();
+ }
+
+ async handleClearCache() {
+ const currentSrc = window.location.href;
+
+ const uploads = this.stores.uploads.filterByIndex({src: currentSrc});
+ const groups = this.stores.groups.filterByIndex({src:currentSrc});
+
+ await Promise.all([
+ ...uploads.map(upload => this.clearUpload(upload.id)),
+ ...groups.map(group => {
+ this.groups.get(group.id)?.element?.remove();
+ this.groups.delete(group.id);
+ return this.stores.groups.delete(group.id);
+ })
+ ]);
+
+ this.a11y.announce('Cache cleared for this page');
+ }
+
+ /**
+ * Get files from all upload fields in a form
+ * Returns array of {file, fieldName, uploadId, meta}
+ */
+ async getFilesForForm(formElement) {
+ const uploadFields = formElement.querySelectorAll(this.selectors.fields.field);
+ const allFiles = [];
+
+ for (const fieldElement of uploadFields) {
+ const fieldId = this.determineFieldId(fieldElement);
+ const fieldName = fieldElement.dataset.field;
+ const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
+
+ for (const upload of uploads) {
+ const file = this.formatFile(upload);
+ if (file) {
+ allFiles.push({
+ file: file,
+ fieldName: fieldName,
+ uploadId: upload.id,
+ meta: upload.fields || {}
+ });
+ }
+ }
}
- this.selectionHandlers.forEach(handler => handler.destroy());
- this.selectionHandlers.clear();
+ return allFiles;
+ }
- this.cleanupAllPreviewUrls();
+ /**
+ * Clear all uploads and groups for a specific field from stores
+ */
+ async clearFieldFromStores(fieldId) {
+ const uploads = this.stores.uploads.filterByIndex({ field: fieldId });
+ const groups = this.stores.groups.filterByIndex({ field: fieldId });
- this.sortableInstances.forEach(instance => {
- if (instance?.destroy) instance.destroy();
- });
- this.sortableInstances.clear();
+ // Clear all uploads
+ await Promise.all(
+ uploads.map(upload => this.clearUpload(upload.id))
+ );
- this.uploadElements.clear();
- this.fieldElements.clear();
- this.groupElements.clear();
- this.selected.clear();
- this.subscribers.clear();
+ // Clear all groups
+ await Promise.all(
+ groups.map(group => {
+ this.groups.get(group.id)?.element?.remove();
+ this.groups.delete(group.id);
+ return this.stores.groups.delete(group.id);
+ })
+ );
}
}
-// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', async function () {
window.auth.subscribe((event) => {
if (event === 'auth-loaded') {
diff --git a/assets/js/concise/UploadManagerC.js b/assets/js/concise/UploadManagerC.js
new file mode 100644
index 0000000..4e755ea
--- /dev/null
+++ b/assets/js/concise/UploadManagerC.js
@@ -0,0 +1,2278 @@
+/**
+ * UploadManager - Refactored with simplified store architecture
+ *
+ * Architecture:
+ * - uploadStore: Individual uploads with blob data, keyed by uploadId
+ * - groupStore: Group metadata + upload references, keyed by groupId
+ * - Runtime Maps: DOM references only (fieldElements, uploadElements, groupElements)
+ *
+ * Flow: File → Process → Store → Queue → Server → Clear stores
+ * Recovery: Check stores on load → Show notification → Restore to DOM
+ */
+class UploadManager {
+ constructor() {
+ this.queue = window.jvbQueue;
+ this.a11y = window.jvbA11y;
+ this.error = window.jvbError;
+
+ // Store initialization flags
+ this.storesReady = false;
+ this.hasCheckedForRecovery = false;
+
+ // Register stores
+ const { uploads, groups } = window.jvbStore.register(
+ 'uploads',
+ [
+ {
+ storeName: 'uploads',
+ keyPath: 'id',
+ storeBlobs: true,
+ indexes: [
+ { name: 'fieldId', keyPath: 'fieldId' },
+ { name: 'status', keyPath: 'status' },
+ { name: 'groupId', keyPath: 'groupId' },
+ { name: 'pageUrl', keyPath: 'pageUrl' }
+ ],
+ TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+ delayFetch: true
+ },
+ {
+ storeName: 'groups',
+ keyPath: 'id',
+ indexes: [
+ { name: 'fieldId', keyPath: 'fieldId' },
+ { name: 'pageUrl', keyPath: 'pageUrl' }
+ ],
+ TTL: 7 * 24 * 60 * 60 * 1000,
+ delayFetch: true
+ }
+ ]
+ );
+
+ this.uploadStore = uploads;
+ this.groupStore = groups;
+
+ // Subscribe to store events
+ this.uploadStore.subscribe(this.handleStoreEvent.bind(this, 'uploads'));
+ this.groupStore.subscribe(this.handleStoreEvent.bind(this, 'groups'));
+
+ // RUNTIME DATA - DOM references only, not persisted
+ this.fieldElements = new Map(); // fieldId → { element, ui, config }
+ this.uploadElements = new Map(); // uploadId → { element, preview, location }
+ this.groupElements = new Map(); // groupId → { element, grid, fieldId }
+
+ // Selection state
+ this.selected = new Map(); // fieldId -> { }
+ this.selectionHandlers = new Map();
+ this.sortableInstances = new Map();
+
+ // Preview URL tracking for cleanup
+ this.previewUrls = new Set();
+
+ // Worker for image processing
+ this.worker = this.createWorkerConfig();
+
+ // Notification subscribers
+ this.subscribers = new Set();
+
+ // Selectors
+ this.selectors = {
+ field: {
+ field: '[data-upload-field]',
+ input: 'input[type="file"]',
+ dropZone: '.file-upload-container',
+ preview: '.item-grid.preview',
+ progress: '.image-progress'
+ },
+ groups: {
+ container: '.upload-group',
+ grid: '.item-grid.group',
+ header: '.group-header',
+ selectAll: '[name="select-all-group"]',
+ actions: '.group-actions',
+ count: '.selection-controls .info'
+ },
+ items: {
+ item: '[data-upload-id]',
+ checkbox: '[name*="select-item"]',
+ featured: '[name="featured"]',
+ details: 'details'
+ }
+ };
+
+ this.statusMapping = {
+ 'received': 'Image Received',
+ 'processing': 'Processing Image...',
+ 'queued': 'Waiting to upload...',
+ 'uploading': 'Uploading to Server',
+ 'pending': 'Sent to server, awaiting processing.',
+ 'server_processing': 'Processing on server...',
+ 'completed': 'Upload complete!',
+ 'failed': 'Upload failed (will retry)',
+ 'failed_permanent': 'Upload failed permanently'
+ };
+
+ this.init();
+ }
+
+ /*******************************************************************************
+ * INITIALIZATION
+ *******************************************************************************/
+
+ async init() {
+ this.initListeners();
+ this.initQueueSubscription();
+
+ window.addEventListener('beforeunload', () => this.cleanupAllPreviewUrls());
+ }
+
+ createWorkerConfig() {
+ return {
+ worker: null,
+ tasks: new Map(),
+ restart: { count: 0, max: 3 },
+ settings: {
+ timeout: 10000,
+ maxConcurrent: 3,
+ restartAfterTimeout: true
+ }
+ };
+ }
+
+ initQueueSubscription() {
+ this.queue.subscribe((event, operation) => {
+ if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+ return;
+ }
+
+ const fieldId = operation.data instanceof FormData
+ ? operation.data.get('fieldId')
+ : operation.data?.fieldId;
+
+ switch (event) {
+ case 'cancel-operation':
+ if (fieldId) this.handleOperationCancelled(fieldId);
+ break;
+ case 'operation-status':
+ if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+ break;
+ case 'operation-complete':
+ this.handleOperationComplete(operation, fieldId);
+ break;
+ case 'operation-failed':
+ case 'operation-failed-permanent':
+ this.handleOperationFailed(operation, fieldId);
+ break;
+ }
+ });
+ }
+
+ /*******************************************************************************
+ * STORE EVENT HANDLING & RECOVERY
+ *******************************************************************************/
+
+ handleStoreEvent(storeName, event, data) {
+ if (event === 'data-loaded') {
+ this.checkStoresReady();
+ }
+ }
+
+ checkStoresReady() {
+ // Both stores need to be ready before checking for recovery
+ const uploadsReady = this.uploadStore.getStore()._initialized;
+ const groupsReady = this.groupStore.getStore()._initialized;
+
+ if (uploadsReady && groupsReady && !this.hasCheckedForRecovery) {
+ this.hasCheckedForRecovery = true;
+ this.storesReady = true;
+ this.checkForRecoverableUploads();
+ }
+ }
+
+ async checkForRecoverableUploads() {
+ const allUploads = this.uploadStore.getAll();
+
+ // Find uploads that weren't completed and don't have an active operation
+ const recoverableUploads = allUploads.filter(upload =>
+ !upload.operationId &&
+ ['processed', 'processing', 'queued'].includes(upload.status)
+ );
+
+ if (recoverableUploads.length === 0) return;
+
+ // Group by fieldId for display
+ const byField = this.groupUploadsByField(recoverableUploads);
+ await this.showRecoveryNotification(byField);
+ }
+
+ groupUploadsByField(uploads) {
+ const byField = new Map();
+
+ uploads.forEach(upload => {
+ if (!byField.has(upload.fieldId)) {
+ byField.set(upload.fieldId, {
+ fieldId: upload.fieldId,
+ pageUrl: upload.pageUrl,
+ uploads: [],
+ groups: new Map()
+ });
+ }
+
+ const fieldData = byField.get(upload.fieldId);
+ fieldData.uploads.push(upload);
+
+ // Track groups
+ if (upload.groupId) {
+ const group = this.groupStore.get(upload.groupId);
+ if (group) {
+ fieldData.groups.set(upload.groupId, group);
+ }
+ }
+ });
+
+ return byField;
+ }
+
+ /*******************************************************************************
+ * FIELD MANAGEMENT - Runtime only, no persistence
+ *******************************************************************************/
+
+ scanFields(container, autoUpload = false) {
+ const fields = container.querySelectorAll(this.selectors.field.field);
+ fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
+ }
+
+ registerUploader(uploader, autoUpload = false) {
+ const fieldId = this.determineFieldId(uploader);
+ const config = this.extractFieldConfig(uploader, autoUpload);
+ const ui = this.buildFieldUI(uploader);
+
+ // Store DOM references only - not persisted
+ this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
+ uploader.dataset.uploader = fieldId;
+ this.addFieldSelectionHandler(fieldId);
+
+ if (config.type !== 'single') {
+ this.initSortable(fieldId);
+ }
+
+ return fieldId;
+ }
+
+ extractFieldConfig(fieldElement, autoUpload) {
+ return {
+ autoUpload,
+ destination: fieldElement.dataset.destination || 'meta',
+ content: fieldElement.dataset.content || null,
+ mode: fieldElement.dataset.mode || 'direct',
+ type: fieldElement.dataset.type || 'single',
+ name: fieldElement.dataset.field,
+ itemID: fieldElement.dataset.itemId || 0,
+ maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+ subtype: fieldElement.dataset.subtype || 'image'
+ };
+ }
+
+ buildFieldUI(fieldElement) {
+ const UI = {
+ field: fieldElement,
+ input: fieldElement.querySelector(this.selectors.field.input),
+ dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+ preview: fieldElement.querySelector(this.selectors.field.preview),
+ progress: {
+ container: fieldElement.querySelector(this.selectors.field.progress),
+ bar: fieldElement.querySelector('.bar'),
+ fill: fieldElement.querySelector('.fill'),
+ details: fieldElement.querySelector('.details'),
+ text: fieldElement.querySelector('.details .text'),
+ count: fieldElement.querySelector('.details .count')
+ }
+ };
+
+ const display = fieldElement.querySelector('.group-display');
+ if (display) {
+ UI.groups = {
+ display,
+ container: fieldElement.querySelector('.item-grid.groups'),
+ empty: fieldElement.querySelector('.empty-group'),
+ groups: new Map()
+ };
+ }
+
+ return UI;
+ }
+
+ /**
+ * Get uploads for a field - derived from store, not cached
+ */
+ getFieldUploads(fieldId) {
+ return this.uploadStore.getAll().filter(u => u.fieldId === fieldId);
+ }
+
+ /**
+ * Get groups for a field - derived from store
+ */
+ getFieldGroups(fieldId) {
+ return this.groupStore.getAll().filter(g => g.fieldId === fieldId);
+ }
+
+ /**
+ * Get upload count for a field
+ */
+ getFieldUploadCount(fieldId) {
+ return this.getFieldUploads(fieldId).length;
+ }
+
+ /*******************************************************************************
+ * FILE PROCESSING
+ *******************************************************************************/
+
+ async processFiles(fieldId, files) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const { config, ui } = fieldEl;
+
+ // Show group display, hide upload zone
+ if (ui.dropZone) ui.dropZone.hidden = true;
+ if (ui.groups?.display) ui.groups.display.hidden = false;
+
+ const totalFiles = files.length;
+ let processedCount = 0;
+
+ this.updateFieldProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+ const processPromises = Array.from(files).map(async (file) => {
+ try {
+ const uploadId = this.generateId('upload');
+
+ // Create upload data
+ const uploadData = {
+ id: uploadId,
+ fieldId,
+ pageUrl: window.location.href,
+ status: 'processing',
+ groupId: null,
+ attachmentId: null,
+ meta: {
+ originalName: file.name,
+ size: file.size,
+ type: file.type
+ }
+ };
+
+ // Save initial state
+ await this.uploadStore.save(uploadData);
+
+ // Process file
+ const preview = this.createPreviewUrl(file);
+ const processedFile = file.type.startsWith('image/')
+ ? await this.processImage(file, uploadId)
+ : file;
+
+ // Show progress
+ this.showUploadProgress(uploadId, true);
+ this.updateUploadItemProgress(uploadId, 50, 'processing');
+
+ // Store blob data
+ await this.saveBlobData(uploadId, processedFile || file);
+
+ // Create DOM element
+ const subtype = this.getSubtypeFromMime(file.type);
+ const element = this.createUploadElement({
+ id: uploadId,
+ preview,
+ meta: uploadData.meta,
+ subtype
+ }, config.destination === 'post_group');
+
+ // Add to preview grid
+ if (ui.preview) {
+ ui.preview.appendChild(element);
+ this.uploadElements.set(uploadId, { element, preview, location: ui.preview });
+ }
+
+ // Update status
+ await this.updateUploadStatus(uploadId, 'processed');
+
+ // Update progress
+ processedCount++;
+ this.updateFieldProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+ this.updateUploadItemProgress(uploadId, 100, 'processed');
+
+ setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
+
+ return uploadId;
+
+ } catch (error) {
+ console.error('Error processing file:', file.name, error);
+ processedCount++;
+ this.updateFieldProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+ return null;
+ }
+ });
+
+ await Promise.all(processPromises);
+
+ this.updateFieldState(fieldId);
+ this.refreshSortable(fieldId);
+
+ // Queue for upload if auto-upload enabled
+ if (config.autoUpload && config.destination !== 'post_group') {
+ await this.queueUpload(fieldId);
+ this.maybeLockUploads(fieldId);
+ }
+ }
+
+ /*******************************************************************************
+ * IMAGE PROCESSING (unchanged logic, just cleaner structure)
+ *******************************************************************************/
+
+ async processImage(file, uploadId) {
+ const timeout = this.worker.settings.timeout;
+
+ return new Promise((resolve, reject) => {
+ let timeoutId;
+ let completed = false;
+
+ timeoutId = setTimeout(() => {
+ if (!completed) {
+ completed = true;
+ this.worker.tasks.delete(uploadId);
+ if (this.worker.settings.restartAfterTimeout) {
+ this.restartWorker();
+ }
+ reject(new Error(`Processing timeout for ${file.name}`));
+ }
+ }, timeout);
+
+ this.worker.tasks.set(uploadId, { file, timeoutId });
+
+ this.handleImageProcess(file, uploadId)
+ .then(result => {
+ if (!completed) {
+ completed = true;
+ clearTimeout(timeoutId);
+ this.worker.tasks.delete(uploadId);
+ resolve(result);
+ }
+ })
+ .catch(error => {
+ if (!completed) {
+ completed = true;
+ clearTimeout(timeoutId);
+ this.worker.tasks.delete(uploadId);
+ reject(error);
+ }
+ });
+ });
+ }
+
+ async handleImageProcess(file, uploadId) {
+ if (!file.type.startsWith('image/')) return file;
+
+ const maxDimension = this.getMaxDimension();
+ const quality = 0.95;
+
+ if (this.shouldUseWorker(file)) {
+ try {
+ if (!this.worker.worker) this.initWorker();
+ if (this.worker.worker) {
+ return await this.processWithWorker(file, uploadId, maxDimension, quality);
+ }
+ } catch (error) {
+ console.warn('Worker failed, using main thread:', error);
+ }
+ }
+
+ return this.processOnMainThread(file, maxDimension, quality);
+ }
+
+ async processOnMainThread(file, maxDimension, quality) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ let objectUrl = null;
+
+ const cleanup = () => {
+ img.onload = img.onerror = null;
+ if (objectUrl) {
+ URL.revokeObjectURL(objectUrl);
+ objectUrl = null;
+ }
+ canvas.width = canvas.height = 1;
+ ctx.clearRect(0, 0, 1, 1);
+ };
+
+ img.onload = () => {
+ try {
+ const { width, height } = this.calculateDimensions(img, maxDimension);
+ canvas.width = width;
+ canvas.height = height;
+
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = 'high';
+ ctx.drawImage(img, 0, 0, width, height);
+
+ const outputFormat = this.getOptimalFormat(file);
+ const outputQuality = this.getOptimalQuality(file, quality);
+
+ canvas.toBlob(
+ (blob) => {
+ cleanup();
+ if (blob) {
+ resolve(new File(
+ [blob],
+ this.getProcessedFileName(file, outputFormat),
+ { type: outputFormat, lastModified: Date.now() }
+ ));
+ } else {
+ reject(new Error('Canvas toBlob failed'));
+ }
+ },
+ outputFormat,
+ outputQuality
+ );
+ } catch (error) {
+ cleanup();
+ reject(new Error(`Canvas processing failed: ${error.message}`));
+ }
+ };
+
+ img.onerror = () => {
+ cleanup();
+ reject(new Error(`Failed to load image: ${file.name}`));
+ };
+
+ try {
+ objectUrl = this.createPreviewUrl(file);
+ img.src = objectUrl;
+ } catch (error) {
+ cleanup();
+ reject(new Error(`Failed to create object URL: ${error.message}`));
+ }
+ });
+ }
+
+ // Worker methods (simplified)
+ initWorker() {
+ if (this.worker.worker || typeof Worker === 'undefined') return;
+
+ try {
+ const workerScript = `
+ self.onmessage = async function(e) {
+ const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+ try {
+ const bitmap = await createImageBitmap(file);
+ const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+ const width = Math.round(bitmap.width * scale);
+ const height = Math.round(bitmap.height * scale);
+ const canvas = new OffscreenCanvas(width, height);
+ const ctx = canvas.getContext('2d');
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = 'high';
+ ctx.drawImage(bitmap, 0, 0, width, height);
+ bitmap.close();
+ const blob = await canvas.convertToBlob({ type: outputFormat, quality });
+ self.postMessage({ messageId, success: true, blob, format: outputFormat });
+ } catch (error) {
+ self.postMessage({ messageId, success: false, error: error.message });
+ }
+ };
+ `;
+
+ const blob = new Blob([workerScript], { type: 'application/javascript' });
+ this.worker.worker = new Worker(this.createPreviewUrl(blob));
+ } catch (error) {
+ console.warn('Failed to initialize worker:', error);
+ this.worker.worker = null;
+ }
+ }
+
+ async processWithWorker(file, uploadId, maxDimension, quality) {
+ return new Promise((resolve, reject) => {
+ if (!this.worker.worker) {
+ reject(new Error('Worker not available'));
+ return;
+ }
+
+ const messageId = `${uploadId}_${Date.now()}`;
+ const outputFormat = this.getOptimalFormat(file);
+
+ const handler = (e) => {
+ if (e.data.messageId !== messageId) return;
+ this.worker.worker.removeEventListener('message', handler);
+ this.worker.worker.removeEventListener('error', errorHandler);
+
+ if (e.data.success) {
+ resolve(new File(
+ [e.data.blob],
+ this.getProcessedFileName(file, e.data.format || outputFormat),
+ { type: e.data.format || outputFormat, lastModified: Date.now() }
+ ));
+ } else {
+ reject(new Error(e.data.error || 'Worker processing failed'));
+ }
+ };
+
+ const errorHandler = (error) => {
+ this.worker.worker.removeEventListener('message', handler);
+ this.worker.worker.removeEventListener('error', errorHandler);
+ reject(new Error(`Worker error: ${error.message}`));
+ };
+
+ this.worker.worker.addEventListener('message', handler);
+ this.worker.worker.addEventListener('error', errorHandler);
+ this.worker.worker.postMessage({ messageId, file, maxDimension, quality, outputFormat });
+ });
+ }
+
+ restartWorker() {
+ if (this.worker.worker) {
+ this.worker.worker.terminate();
+ this.worker.worker = null;
+ }
+ this.worker.tasks.clear();
+
+ if (this.worker.restart.count < this.worker.restart.max) {
+ this.worker.restart.count++;
+ this.initWorker();
+ }
+ }
+
+ // Image processing helpers
+ calculateDimensions(img, maxDimension) {
+ let { width, height } = img;
+ if (width <= maxDimension && height <= maxDimension) {
+ return { width, height };
+ }
+ const scale = Math.min(maxDimension / width, maxDimension / height);
+ return { width: Math.round(width * scale), height: Math.round(height * scale) };
+ }
+
+ getMaxDimension() {
+ const screenWidth = window.screen.width;
+ const dpr = window.devicePixelRatio || 1;
+ if (screenWidth * dpr > 2560) return 2400;
+ if (screenWidth * dpr > 1920) return 1920;
+ return 1200;
+ }
+
+ shouldUseWorker(file) {
+ return this.worker.worker && file.size > 1024 * 1024 && typeof OffscreenCanvas !== 'undefined';
+ }
+
+ getOptimalFormat(file) {
+ if (file.type === 'image/gif' || file.type === 'image/svg+xml') return file.type;
+ return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+ }
+
+ getOptimalQuality(file, requestedQuality) {
+ if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+ if (file.size < 2 * 1024 * 1024) return requestedQuality;
+ return Math.min(requestedQuality, 0.8);
+ }
+
+ getProcessedFileName(file, outputFormat) {
+ const baseName = file.name.replace(/\.[^/.]+$/, '');
+ const extensions = { 'image/webp': '.webp', 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif' };
+ return baseName + (extensions[outputFormat] || '.jpg');
+ }
+
+ supportsWebP() {
+ const canvas = document.createElement('canvas');
+ return canvas.toDataURL('image/webp').startsWith('data:image/webp');
+ }
+
+ /*******************************************************************************
+ * BLOB DATA MANAGEMENT
+ *******************************************************************************/
+
+ async saveBlobData(uploadId, file) {
+ const arrayBuffer = await file.arrayBuffer();
+ const upload = this.uploadStore.get(uploadId) || { id: uploadId };
+
+ upload.blobData = {
+ buffer: arrayBuffer,
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ lastModified: file.lastModified || Date.now()
+ };
+
+ await this.uploadStore.save(upload);
+ }
+
+ async getBlobData(uploadId) {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload?.blobData) return null;
+
+ const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
+ return new File([blob], upload.blobData.name, {
+ type: upload.blobData.type,
+ lastModified: upload.blobData.lastModified
+ });
+ }
+
+ /*******************************************************************************
+ * QUEUE INTEGRATION
+ *******************************************************************************/
+
+ async queueUpload(fieldId) {
+ const uploads = this.getFieldUploads(fieldId);
+ if (uploads.length === 0) return;
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const formData = await this.prepareUploadFormData(fieldId, uploads, fieldEl.config);
+
+ this.a11y.announce('Queuing for upload');
+
+ const operation = {
+ endpoint: 'uploads',
+ method: 'POST',
+ data: formData,
+ title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+ popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+ canMerge: false,
+ headers: { 'action_nonce': window.auth.getNonce('dash') },
+ append: '_upload'
+ };
+
+ try {
+ const operationId = await this.queue.addToQueue(operation);
+
+ // Update upload statuses
+ for (const upload of uploads) {
+ upload.operationId = operationId;
+ upload.status = 'queued';
+ await this.uploadStore.save(upload);
+ this.updateUploadUI(upload.id);
+ }
+
+ return operationId;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async prepareUploadFormData(fieldId, uploads, config) {
+ const formData = new FormData();
+
+ formData.append('content', config.content);
+ formData.append('mode', config.mode);
+ formData.append('field_name', config.name);
+ formData.append('fieldId', fieldId);
+ formData.append('field_type', config.type);
+ formData.append('subtype', config.subtype);
+ formData.append('item_id', config.itemID);
+ formData.append('destination', config.destination || 'meta');
+
+ const uploadMap = [];
+
+ for (const upload of uploads) {
+ const file = await this.getBlobData(upload.id);
+ if (file) {
+ formData.append('files[]', file);
+ uploadMap.push(upload.id);
+ }
+ }
+
+ formData.append('upload_ids', JSON.stringify(uploadMap));
+ return formData;
+ }
+
+ async submitGroupedUploads(fieldId) {
+ const uploads = this.getFieldUploads(fieldId);
+ const groups = this.getFieldGroups(fieldId);
+ const fieldEl = this.fieldElements.get(fieldId);
+
+ if (uploads.length === 0 || !fieldEl) return;
+
+ const formData = new FormData();
+ const posts = [];
+ const uploadMap = [];
+
+ // Process each group
+ for (const group of groups) {
+ const groupUploads = uploads.filter(u => u.groupId === group.id);
+
+ const post = {
+ images: [],
+ fields: { ...group.fields }
+ };
+
+ for (const upload of groupUploads) {
+ const file = await this.getBlobData(upload.id);
+ if (file) {
+ formData.append('files[]', file);
+ post.images.push({ upload_id: upload.id, index: uploadMap.length });
+ uploadMap.push(upload.id);
+
+ // Check featured
+ const uploadEl = this.uploadElements.get(upload.id);
+ const featured = uploadEl?.element?.querySelector('[name="featured"]');
+ if (featured?.checked) {
+ post.fields.featured = upload.id;
+ }
+ }
+ }
+
+ if (post.images.length > 0) {
+ posts.push(post);
+ }
+ }
+
+ // Handle ungrouped uploads - each becomes its own post
+ const ungrouped = uploads.filter(u => !u.groupId);
+ for (const upload of ungrouped) {
+ const file = await this.getBlobData(upload.id);
+ if (file) {
+ formData.append('files[]', file);
+ posts.push({
+ images: [{ upload_id: upload.id, index: uploadMap.length }],
+ fields: {}
+ });
+ uploadMap.push(upload.id);
+ }
+ }
+
+ formData.append('content', fieldEl.config.content);
+ formData.append('user', fieldEl.config.itemID);
+ formData.append('posts', JSON.stringify(posts));
+ formData.append('upload_ids', JSON.stringify(uploadMap));
+
+ const operation = {
+ endpoint: 'uploads/groups',
+ method: 'POST',
+ data: formData,
+ title: `Creating ${posts.length} ${fieldEl.config.content}${posts.length > 1 ? 's' : ''}...`,
+ popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+ canMerge: false,
+ headers: { 'action_nonce': window.auth.getNonce('dash') },
+ append: '_upload'
+ };
+
+ try {
+ const operationId = await this.queue.addToQueue(operation);
+
+ for (const upload of uploads) {
+ upload.operationId = operationId;
+ upload.status = 'queued';
+ await this.uploadStore.save(upload);
+ this.updateUploadUI(upload.id);
+ }
+
+ this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''}`);
+ return operationId;
+ } catch (error) {
+ this.error.log(error, { component: 'UploadManager', action: 'submitGroupedUploads', fieldId });
+ throw error;
+ }
+ }
+
+ async queueUploadMeta(e) {
+ const uploadId = this.getUploadIdFromElement(e.target);
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) return;
+
+ const data = { [e.target.name]: e.target.value };
+ upload.meta = { ...upload.meta, ...data };
+ await this.uploadStore.save(upload);
+
+ const queueData = { [upload.attachmentId ?? upload.id]: upload.meta };
+
+ const operation = {
+ endpoint: 'uploads/meta',
+ method: 'POST',
+ data: queueData,
+ title: 'Updating meta',
+ canMerge: true,
+ headers: { 'action_nonce': window.auth.getNonce('dash') }
+ };
+
+ try {
+ await this.queue.addToQueue(operation);
+ } catch (error) {
+ this.error.log(error, { component: 'UploadManager', action: 'queueUploadMeta', uploadId });
+ }
+ }
+
+ /*******************************************************************************
+ * QUEUE EVENT HANDLERS - Cleanup after success
+ *******************************************************************************/
+
+ async handleOperationComplete(operation, fieldId) {
+ const results = operation.result?.data || operation.serverData?.data || [];
+
+ // Update attachment IDs
+ for (const result of results) {
+ const upload = this.uploadStore.get(result.upload_id);
+ if (upload) {
+ upload.attachmentId = result.attachment_id;
+ upload.status = 'completed';
+ await this.uploadStore.save(upload);
+ this.updateUploadUI(result.upload_id);
+ }
+ }
+
+ if (!fieldId) return;
+
+ // Clear completed uploads and their groups
+ await this.clearCompletedUploads(fieldId);
+
+ this.updateFieldState(fieldId);
+ this.a11y.announce('All uploads completed successfully');
+ }
+
+ async clearCompletedUploads(fieldId) {
+ const uploads = this.getFieldUploads(fieldId);
+ const groupIds = new Set();
+
+ for (const upload of uploads) {
+ if (upload.status === 'completed') {
+ if (upload.groupId) groupIds.add(upload.groupId);
+ await this.clearUpload(upload.id);
+ }
+ }
+
+ // Clear associated groups
+ for (const groupId of groupIds) {
+ await this.groupStore.delete(groupId);
+ this.groupElements.delete(groupId);
+ }
+ }
+
+ handleOperationFailed(operation, fieldId) {
+ const uploadIds = operation.data instanceof FormData
+ ? JSON.parse(operation.data.get('upload_ids') || '[]')
+ : operation.data?.upload_ids || [];
+
+ const status = operation.status === 'operation-failed-permanent' ? 'failed_permanent' : 'failed';
+
+ uploadIds.forEach(async uploadId => {
+ await this.updateUploadStatus(uploadId, status);
+ });
+
+ if (fieldId) this.updateFieldState(fieldId);
+ }
+
+ async handleOperationCancelled(fieldId) {
+ const uploads = this.getFieldUploads(fieldId);
+ const groups = this.getFieldGroups(fieldId);
+
+ for (const upload of uploads) {
+ await this.clearUpload(upload.id);
+ }
+
+ for (const group of groups) {
+ await this.groupStore.delete(group.id);
+ this.groupElements.delete(group.id);
+ }
+
+ this.updateFieldState(fieldId);
+ this.a11y.announce('Upload cancelled');
+ }
+
+ /*******************************************************************************
+ * GROUP MANAGEMENT
+ *******************************************************************************/
+
+ async createGroup(fieldId, groupId = null) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return null;
+
+ groupId = groupId || this.generateId('group');
+
+ // Create group data
+ const groupData = {
+ id: groupId,
+ fieldId,
+ pageUrl: window.location.href,
+ uploads: [],
+ fields: {}
+ };
+
+ await this.groupStore.save(groupData);
+
+ // Create DOM element
+ const element = this.createGroupElement(groupId, fieldId);
+ if (!element) return null;
+
+ const grid = element.querySelector('.item-grid.group');
+
+ // Store DOM references
+ this.groupElements.set(groupId, { element, grid, fieldId });
+
+ // Insert into DOM
+ if (fieldEl.ui.groups?.container && fieldEl.ui.groups.empty) {
+ fieldEl.ui.groups.container.insertBefore(element, fieldEl.ui.groups.empty);
+ } else if (fieldEl.ui.groups?.container) {
+ fieldEl.ui.groups.container.appendChild(element);
+ }
+
+ // Initialize sortable and selection
+ this.addGroupSelectionHandler(fieldId, groupId);
+ if (grid) this.createSortableForGrid(grid, fieldId, groupId);
+
+ return { id: groupId, element, grid };
+ }
+
+ createGroupElement(groupId, fieldId) {
+ const element = window.getTemplate('imageGroup');
+ if (!element) return null;
+
+ element.dataset.groupId = groupId;
+ element.dataset.fieldId = fieldId;
+
+ const fields = window.getTemplate('groupMetadata');
+ const fieldsContainer = element.querySelector('.fields');
+
+ if (fieldsContainer && fields) {
+ fieldsContainer.append(fields);
+
+ const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+ const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+ if (titleInput) {
+ titleInput.id = `${groupId}_title`;
+ titleInput.name = `${groupId}[post_title]`;
+ }
+ if (excerptInput) {
+ excerptInput.id = `${groupId}_excerpt`;
+ excerptInput.name = `${groupId}[post_excerpt]`;
+ }
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (fieldEl?.config.content) {
+ const summary = element.querySelector('summary');
+ if (summary) summary.textContent = fieldEl.config.content + ' Fields';
+ }
+ } else {
+ element.querySelector('details')?.remove();
+ }
+
+ const gridContainer = element.querySelector('.item-grid.group');
+ if (gridContainer) gridContainer.dataset.groupId = groupId;
+
+ return element;
+ }
+
+ async deleteGroup(groupId, moveUploadsToPreview = true) {
+ const groupEl = this.groupElements.get(groupId);
+ if (!groupEl) return;
+
+ const group = this.groupStore.get(groupId);
+ const fieldEl = this.fieldElements.get(groupEl.fieldId);
+
+ if (moveUploadsToPreview && group?.uploads) {
+ for (const uploadId of group.uploads) {
+ await this.removeFromGroup(uploadId);
+ }
+ }
+
+ // Remove from store
+ await this.groupStore.delete(groupId);
+
+ // Remove DOM element
+ groupEl.element?.remove();
+
+ // Cleanup
+ this.groupElements.delete(groupId);
+
+ const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+ this.sortableInstances.get(sortableKey)?.destroy();
+ this.sortableInstances.delete(sortableKey);
+
+ this.a11y.announce('Group removed');
+ }
+
+ async addToGroup(uploadId, targetGrid, persist = true) {
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!upload || !uploadEl) return;
+
+ const groupId = targetGrid.dataset.groupId;
+ const group = this.groupStore.get(groupId);
+
+ // Remove from previous group if needed
+ if (upload.groupId && upload.groupId !== groupId) {
+ const oldGroup = this.groupStore.get(upload.groupId);
+ if (oldGroup) {
+ oldGroup.uploads = oldGroup.uploads.filter(id => id !== uploadId);
+ if (oldGroup.uploads.length === 0) {
+ await this.deleteGroup(upload.groupId, false);
+ } else {
+ await this.groupStore.save(oldGroup);
+ }
+ }
+ }
+
+ // Add to new group
+ upload.groupId = groupId;
+ if (group && !group.uploads.includes(uploadId)) {
+ group.uploads.push(uploadId);
+ await this.groupStore.save(group);
+ }
+
+ // Update upload
+ await this.uploadStore.save(upload);
+
+ // Move DOM element
+ targetGrid.appendChild(uploadEl.element);
+ uploadEl.location = targetGrid;
+
+ // Show featured radio
+ const featured = uploadEl.element.querySelector('[name="featured"]');
+ if (featured) {
+ featured.hidden = false;
+ featured.name = `${groupId}_featured`;
+ }
+
+ // Clear checkbox
+ const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+ if (checkbox) checkbox.checked = false;
+
+ this.updateSortableState(targetGrid);
+ }
+
+ async removeFromGroup(uploadId) {
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!upload || !uploadEl) return;
+
+ const fieldEl = this.fieldElements.get(upload.fieldId);
+ if (!fieldEl?.ui?.preview) return;
+
+ // Update group
+ if (upload.groupId) {
+ const group = this.groupStore.get(upload.groupId);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ await this.deleteGroup(upload.groupId, false);
+ } else {
+ await this.groupStore.save(group);
+ }
+ }
+ upload.groupId = null;
+ await this.uploadStore.save(upload);
+ }
+
+ // Move to preview
+ fieldEl.ui.preview.appendChild(uploadEl.element);
+ uploadEl.location = fieldEl.ui.preview;
+
+ // Hide featured radio
+ const featured = uploadEl.element.querySelector('[name="featured"]');
+ if (featured) {
+ featured.hidden = true;
+ featured.checked = false;
+ }
+
+ this.updateSortableState(fieldEl.ui.preview);
+ }
+
+ async removeUpload(fieldId, uploadId) {
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+
+ if (upload?.groupId) {
+ const group = this.groupStore.get(upload.groupId);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ await this.deleteGroup(upload.groupId, false);
+ } else {
+ await this.groupStore.save(group);
+ }
+ }
+ }
+
+ uploadEl?.element?.remove();
+ await this.clearUpload(uploadId);
+
+ this.updateFieldState(fieldId);
+ this.maybeLockUploads(fieldId);
+
+ this.selectionHandlers.get(fieldId)?.deselect(uploadId);
+ this.a11y.announce('Upload removed');
+ }
+
+ async handleGroupMetaChange(input) {
+ const groupEl = input.closest(this.selectors.groups.container);
+ if (!groupEl) return;
+
+ const groupId = groupEl.dataset.groupId;
+ const group = this.groupStore.get(groupId);
+ if (!group) return;
+
+ let name = input.name;
+ if (name.includes(groupId)) {
+ name = name.replace(`${groupId}_`, '').replace(`${groupId}[`, '').replace(']', '');
+ }
+
+ group.fields[name] = input.value;
+ await this.groupStore.save(group);
+ }
+
+ /*******************************************************************************
+ * SORTABLE & DRAG/DROP
+ *******************************************************************************/
+
+ initSortable(fieldId) {
+ if (!window.Sortable) return;
+
+ if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+ Sortable.mount(new Sortable.MultiDrag());
+ Sortable._multiDragMounted = true;
+ }
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+ grids.forEach(grid => {
+ const groupId = grid.classList.contains('group')
+ ? grid.closest('.upload-group')?.dataset.groupId
+ : null;
+ this.createSortableForGrid(grid, fieldId, groupId);
+ });
+
+ // Empty group drop zone
+ const emptyGroup = fieldEl.element.querySelector('.empty-group');
+ if (emptyGroup && !emptyGroup.sortableInstance) {
+ emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+ animation: 150,
+ draggable: '.item',
+ multiDrag: true,
+ selectedClass: 'selected-for-drag',
+ avoidImplicitDeselect: true,
+ group: { name: fieldId, pull: false, put: true },
+ ghostClass: 'sortable-ghost',
+ onEnd: (evt) => this.handleDrop(evt, fieldId)
+ });
+ }
+ }
+
+ createSortableForGrid(grid, fieldId, groupId = null) {
+ if (!grid || grid.sortableInstance) return;
+
+ const instance = new Sortable(grid, {
+ animation: 150,
+ draggable: '.item',
+ multiDrag: true,
+ selectedClass: 'selected-for-drag',
+ avoidImplicitDeselect: true,
+ group: { name: fieldId, pull: true, put: true },
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+ onEnd: (evt) => this.handleDrop(evt, fieldId),
+ onSelect: (evt) => this.syncCheckboxToSortable(evt.item, true),
+ onDeselect: (evt) => this.syncCheckboxToSortable(evt.item, false),
+ onAdd: (evt) => this.updateSortableState(evt.to),
+ onRemove: (evt) => this.updateSortableState(evt.from)
+ });
+
+ grid.sortableInstance = instance;
+
+ const gridId = groupId ? `${fieldId}-group-${groupId}` : `${fieldId}-preview`;
+ this.sortableInstances.set(gridId, instance);
+
+ return instance;
+ }
+
+ syncCheckboxToSortable(item, selected) {
+ const checkbox = item.querySelector('[name*="select-item"]');
+ if (checkbox && checkbox.checked !== selected) {
+ checkbox.checked = selected;
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ }
+
+ /**
+ * Unified drop handler - consolidated from multiple methods
+ */
+ async handleDrop(evt, fieldId) {
+ const target = evt.to;
+ const source = evt.from;
+ const items = evt.items?.length > 0 ? evt.items : [evt.item];
+ const uploadIds = items.map(item => item.dataset.uploadId);
+
+ // Same container = reorder only
+ if (target === source) {
+ this.handleReorder(evt);
+ return;
+ }
+
+ const targetType = this.getDropTargetType(target);
+
+ try {
+ switch (targetType) {
+ case 'empty-group':
+ const group = await this.createGroup(fieldId);
+ if (!group) throw new Error('Group creation failed');
+ for (const uploadId of uploadIds) {
+ await this.addToGroup(uploadId, group.grid, false);
+ }
+ break;
+
+ case 'preview':
+ for (const uploadId of uploadIds) {
+ await this.removeFromGroup(uploadId);
+ }
+ break;
+
+ case 'group':
+ for (const uploadId of uploadIds) {
+ await this.addToGroup(uploadId, target, false);
+ }
+ break;
+
+ default:
+ throw new Error('Unknown drop target');
+ }
+
+ this.finalizeDrop(fieldId, items.length, targetType);
+
+ } catch (error) {
+ console.error('Drop error:', error);
+ // Return items to preview as fallback
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (fieldEl?.ui?.preview) {
+ items.forEach(item => fieldEl.ui.preview.appendChild(item));
+ }
+ this.a11y.announce('An error occurred. Items returned to preview.');
+ }
+
+ this.updateSortableState(target);
+ if (source !== target) this.updateSortableState(source);
+ }
+
+ finalizeDrop(fieldId, count, targetType) {
+ const messages = {
+ 'empty-group': count > 1 ? `Created group with ${count} items` : 'Created group with item',
+ 'preview': count > 1 ? `Moved ${count} items to preview` : 'Moved item to preview',
+ 'group': count > 1 ? `Moved ${count} items to group` : 'Moved item to group'
+ };
+
+ this.a11y.announce(messages[targetType] || 'Items moved');
+ this.selectionHandlers.get(fieldId)?.clearSelection();
+ }
+
+ getDropTargetType(target) {
+ if (target.classList.contains('empty-group')) return 'empty-group';
+ if (target.classList.contains('preview')) return 'preview';
+ if (target.classList.contains('group')) return 'group';
+ return 'unknown';
+ }
+
+ handleReorder(evt) {
+ const grid = evt.to;
+ const fieldWrapper = grid.closest('.field, .upload');
+ if (!fieldWrapper) return;
+
+ const items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+ .map(el => el.dataset.uploadId)
+ .filter(Boolean);
+
+ const hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+ if (hiddenInput && items.length > 0) {
+ hiddenInput.value = items.join(',');
+ }
+
+ // Update group order in store
+ const groupId = grid.dataset.groupId;
+ if (groupId) {
+ const group = this.groupStore.get(groupId);
+ if (group) {
+ group.uploads = items;
+ this.groupStore.save(group);
+ }
+ }
+
+ this.a11y.announce('Item reordered');
+
+ fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+ detail: { from: evt.from, to: evt.to, items },
+ bubbles: true
+ }));
+ }
+
+ updateSortableState(grid) {
+ const sortable = grid?.sortableInstance;
+ if (sortable) sortable.option('disabled', false);
+ }
+
+ refreshSortable(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+ grids.forEach(grid => this.updateSortableState(grid));
+ }
+
+ syncSortableSelection(fieldId, selectedItems) {
+ this.sortableInstances.forEach((instance, key) => {
+ if (key.startsWith(fieldId)) {
+ instance.el.querySelectorAll('.item').forEach(item => {
+ const uploadId = item.dataset.uploadId;
+ if (selectedItems.has(uploadId)) {
+ Sortable.utils.select(item);
+ } else {
+ Sortable.utils.deselect(item);
+ }
+ });
+ }
+ });
+ }
+
+ /*******************************************************************************
+ * EVENT LISTENERS
+ *******************************************************************************/
+
+ initListeners() {
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.dragEnterHandler = this.handleDragEnter.bind(this);
+ this.dragLeaveHandler = this.handleDragLeave.bind(this);
+ this.dragOverHandler = this.handleDragOver.bind(this);
+ this.dropHandler = this.handleExternalDrop.bind(this);
+
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ document.addEventListener('dragenter', this.dragEnterHandler);
+ document.addEventListener('dragleave', this.dragLeaveHandler);
+ document.addEventListener('dragover', this.dragOverHandler);
+ document.addEventListener('drop', this.dropHandler);
+ }
+
+ handleClick(e) {
+ // Trigger file input
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone && !e.target.matches('input, button, a')) {
+ dropZone.querySelector(this.selectors.field.input)?.click();
+ }
+
+ // Action buttons
+ const actionButton = e.target.closest('[data-action]');
+ if (actionButton) this.handleAction(actionButton);
+ }
+
+ handleChange(e) {
+ const fieldId = this.getFieldIdFromElement(e.target);
+ if (!fieldId) return;
+
+ // File input
+ if (e.target.matches(this.selectors.field.input)) {
+ const files = Array.from(e.target.files);
+ if (files.length > 0) this.processFiles(fieldId, files);
+ return;
+ }
+
+ // Meta field changes
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl?.config.autoUpload) return;
+
+ if (fieldEl.config.destination === 'post_group') {
+ this.handleGroupMetaChange(e.target);
+ } else {
+ this.queueUploadMeta(e);
+ }
+ }
+
+ handleDragEnter(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ dropZone.classList.add('dragover');
+ }
+ }
+
+ handleDragLeave(e) {
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone && !dropZone.contains(e.relatedTarget)) {
+ dropZone.classList.remove('dragover');
+ }
+ }
+
+ handleDragOver(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ }
+ }
+
+ handleExternalDrop(e) {
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (!dropZone) return;
+
+ e.preventDefault();
+ dropZone.classList.remove('dragover');
+
+ const files = Array.from(e.dataTransfer.files);
+ if (files.length === 0) return;
+
+ const fieldId = this.getFieldIdFromElement(dropZone);
+ if (fieldId) {
+ this.processFiles(fieldId, files);
+ this.a11y.announce(`${files.length} file(s) dropped for upload`);
+ }
+ }
+
+ /*******************************************************************************
+ * ACTION HANDLERS
+ *******************************************************************************/
+
+ handleAction(button) {
+ const action = button.dataset.action;
+ const fieldId = this.getFieldIdFromElement(button);
+
+ switch (action) {
+ case 'add-to-group':
+ this.handleAddToGroup(fieldId);
+ break;
+ case 'delete-group':
+ this.handleDeleteGroup(button);
+ break;
+ case 'delete-upload':
+ case 'remove-from-group':
+ this.handleRemoveItem(button);
+ break;
+ case 'upload':
+ this.handleSubmitUploads(fieldId);
+ break;
+ case 'restore':
+ this.handleRestoreSelected();
+ break;
+ case 'restore-all':
+ this.handleRestoreAll();
+ break;
+ case 'clear-cache':
+ this.handleClearCache();
+ break;
+ }
+ }
+
+ async handleAddToGroup(fieldId) {
+ const selected = this.selected.get(fieldId);
+
+ if (!selected || selected.size === 0) {
+ await this.createGroup(fieldId);
+ } else {
+ const group = await this.createGroup(fieldId);
+ if (!group) return;
+
+ for (const uploadId of selected) {
+ await this.addToGroup(uploadId, group.grid, false);
+ }
+
+ this.selectionHandlers.get(fieldId)?.clearSelection();
+ this.a11y.announce(`Created group with ${selected.size} items`);
+ }
+ }
+
+ async handleDeleteGroup(button) {
+ const group = button.closest(this.selectors.groups.container);
+ if (!group) return;
+
+ const groupId = group.dataset.groupId;
+
+ if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+ return;
+ }
+
+ await this.deleteGroup(groupId, true);
+ }
+
+ async handleRemoveItem(button) {
+ const item = button.closest(this.selectors.items.item);
+ if (!item) return;
+
+ if (!confirm('Remove this item?')) return;
+
+ const uploadId = item.dataset.uploadId;
+ const fieldId = this.getFieldIdFromElement(item);
+
+ await this.removeUpload(fieldId, uploadId);
+ }
+
+ handleSubmitUploads(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ fieldEl.element.closest('details')?.removeAttribute('open');
+ document.body.classList.add('uploading');
+
+ if (fieldEl.config.destination === 'post_group') {
+ this.submitGroupedUploads(fieldId);
+ } else {
+ this.queueUpload(fieldId);
+ }
+ }
+
+ /*******************************************************************************
+ * SELECTION MANAGEMENT
+ *******************************************************************************/
+
+ addFieldSelectionHandler(fieldId) {
+ if (this.selectionHandlers.has(fieldId)) return;
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl?.element) return;
+
+ const handler = new window.jvbHandleSelection({
+ container: fieldEl.element,
+ ui: {
+ selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
+ bulkControls: fieldEl.element.querySelector('.selection-actions'),
+ count: fieldEl.element.querySelector('.selection-count')
+ },
+ itemSelector: '[data-upload-id]',
+ checkboxSelector: '[name*="select-item"]'
+ });
+
+ handler.subscribe((event, data) => {
+ if (['item-selected', 'item-deselected', 'range-selected'].includes(event)) {
+ this.syncSortableSelection(fieldId, data.selectedItems);
+ this.selected.set(fieldId, data.selectedItems);
+ }
+ });
+
+ this.selectionHandlers.set(fieldId, handler);
+ }
+
+ addGroupSelectionHandler(fieldId, groupId) {
+ const key = `${fieldId}_${groupId}`;
+ if (this.selectionHandlers.has(key)) return;
+
+ const groupEl = this.groupElements.get(groupId);
+ if (!groupEl?.element) return;
+
+ const handler = new window.jvbHandleSelection({
+ container: groupEl.element,
+ ui: {
+ selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
+ bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
+ count: groupEl.element.querySelector(this.selectors.groups.count)
+ },
+ itemSelector: '[data-upload-id]',
+ checkboxSelector: '[name*="select-item"]'
+ });
+
+ handler.subscribe((event, data) => {
+ if (['item-selected', 'item-deselected', 'range-selected'].includes(event)) {
+ this.selected.set(fieldId, data.selectedItems);
+ }
+ });
+
+ this.selectionHandlers.set(key, handler);
+ }
+
+ /*******************************************************************************
+ * UI UPDATES
+ *******************************************************************************/
+
+ updateFieldState(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const uploadCount = this.getFieldUploadCount(fieldId);
+ const groupCount = this.getFieldGroups(fieldId).length;
+
+ fieldEl.element.dataset.hasUploads = uploadCount > 0;
+ fieldEl.element.dataset.uploadCount = uploadCount;
+ fieldEl.element.dataset.hasGroups = groupCount > 0;
+
+ if (fieldEl.ui.preview) {
+ fieldEl.ui.preview.setAttribute('aria-label',
+ `Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+ );
+ }
+ }
+
+ updateFieldProgress(fieldId, current, total, message) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ const progress = fieldEl?.ui?.progress;
+ if (!progress?.container) return;
+
+ const percent = total > 0 ? (current / total) * 100 : 0;
+
+ if (progress.fill) progress.fill.style.width = `${percent}%`;
+ if (progress.text) progress.text.textContent = message;
+ if (progress.count) progress.count.textContent = `${current}/${total}`;
+
+ progress.container.hidden = current === total;
+ }
+
+ updateFieldStatus(fieldId, status) {
+ // Status is now derived from uploads, no field-level status needed
+ }
+
+ async updateUploadStatus(uploadId, status) {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) return;
+
+ upload.status = status;
+ await this.uploadStore.save(upload);
+ this.updateUploadUI(uploadId);
+ }
+
+ updateUploadUI(uploadId) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload || !uploadEl?.element) return;
+
+ // Update class
+ uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+ uploadEl.element.classList.add(`status-${upload.status}`);
+
+ // Update progress
+ const progress = uploadEl.element.querySelector('.progress');
+ if (progress) {
+ this.updateUploadItemProgress(uploadId, this.getStatusProgress(upload.status), upload.status);
+ }
+ }
+
+ showUploadProgress(uploadId, show = true) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ const progress = uploadEl?.element?.querySelector('.progress');
+ if (!progress) return;
+
+ if (show) {
+ progress.style.removeProperty('animation');
+ progress.hidden = false;
+ } else {
+ progress.style.animation = 'fadeOut var(--transition-base)';
+ setTimeout(() => { progress.hidden = true; }, 300);
+ }
+ }
+
+ updateUploadItemProgress(uploadId, percent, status = null) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ const progress = uploadEl?.element?.querySelector('.progress');
+ if (!progress) return;
+
+ const fill = progress.querySelector('.fill');
+ const details = progress.querySelector('.details');
+ const icon = progress.querySelector('.icon');
+
+ if (fill) fill.style.width = `${percent}%`;
+ if (status && details) details.textContent = this.statusMapping[status] || status;
+ if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+ }
+
+ maybeLockUploads(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl?.ui?.dropZone) return;
+
+ const uploadCount = this.getFieldUploadCount(fieldId);
+ const maxFiles = fieldEl.config.destination === 'post_group' ? 20 : (fieldEl.config.maxFiles || 999);
+
+ fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+ fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+ if (fieldEl.config.destination === 'post_group' && uploadCount >= maxFiles) {
+ this.a11y.announce('Maximum of 20 uploads reached.');
+ }
+ }
+
+ /*******************************************************************************
+ * CLEANUP
+ *******************************************************************************/
+
+ async clearUpload(uploadId) {
+ const uploadEl = this.uploadElements.get(uploadId);
+
+ if (uploadEl) {
+ this.revokePreviewUrl(uploadEl.preview);
+ if (uploadEl.element?.dataset.previewUrl) {
+ this.revokePreviewUrl(uploadEl.element.dataset.previewUrl);
+ }
+ }
+
+ this.uploadElements.delete(uploadId);
+ await this.uploadStore.delete(uploadId);
+ }
+
+ createPreviewUrl(file) {
+ const url = URL.createObjectURL(file);
+ this.previewUrls.add(url);
+ return url;
+ }
+
+ revokePreviewUrl(url) {
+ if (url?.startsWith('blob:')) {
+ URL.revokeObjectURL(url);
+ this.previewUrls.delete(url);
+ }
+ }
+
+ cleanupAllPreviewUrls() {
+ this.previewUrls.forEach(url => {
+ try { URL.revokeObjectURL(url); } catch (e) {}
+ });
+ this.previewUrls.clear();
+ }
+
+ /*******************************************************************************
+ * RECOVERY & RESTORATION
+ *******************************************************************************/
+
+ async showRecoveryNotification(byField) {
+ let totalUploads = 0;
+ let totalGroups = 0;
+
+ byField.forEach(field => {
+ totalUploads += field.uploads.length;
+ totalGroups += field.groups.size;
+ });
+
+ const notification = window.getTemplate('restoreNotification');
+ if (!notification) return;
+
+ const message = totalGroups > 0
+ ? `${totalGroups} group${totalGroups > 1 ? 's' : ''} with ${totalUploads} upload${totalUploads > 1 ? 's' : ''} can be restored.`
+ : `${totalUploads} upload${totalUploads > 1 ? 's' : ''} can be recovered.`;
+
+ const detailsEl = notification.querySelector('.restore-details');
+ if (detailsEl) detailsEl.textContent = message;
+
+ // Build preview
+ for (const [fieldId, fieldData] of byField) {
+ const fieldTemplate = window.getTemplate('restoreField');
+ if (!fieldTemplate) continue;
+
+ const titleEl = fieldTemplate.querySelector('h3');
+ if (titleEl) titleEl.textContent = fieldId;
+
+ const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+ for (const upload of fieldData.uploads) {
+ const file = await this.getBlobData(upload.id);
+ if (!file) continue;
+
+ const uploadItem = this.createUploadElement({
+ id: upload.id,
+ preview: this.createPreviewUrl(file),
+ meta: upload.meta,
+ subtype: this.getSubtypeFromMime(file.type)
+ }, false);
+
+ uploadItem.dataset.fieldId = fieldId;
+ itemGrid?.appendChild(uploadItem);
+ }
+
+ notification.querySelector('.wrap')?.appendChild(fieldTemplate);
+ }
+
+ document.querySelector('.field.upload')?.appendChild(notification);
+
+ const dialog = document.querySelector('dialog.restore-uploads');
+ if (dialog) {
+ this.restoreModal = new window.jvbModal(dialog);
+ this.restoreSelection = new window.jvbHandleSelection({
+ container: dialog,
+ ui: {
+ selectAll: dialog.querySelector('#select-all-restore'),
+ count: dialog.querySelector('.selection-count')
+ }
+ });
+ this.restoreModal.handleOpen();
+ }
+ }
+
+ async handleRestoreSelected() {
+ const dialog = document.querySelector('dialog.restore-uploads');
+ if (!dialog) return;
+
+ const selected = [];
+ dialog.querySelectorAll('[type=checkbox]:checked').forEach(checkbox => {
+ const item = checkbox.closest('.item');
+ if (item) {
+ selected.push({
+ uploadId: item.dataset.uploadId,
+ fieldId: item.dataset.fieldId
+ });
+ }
+ });
+
+ if (selected.length > 0) {
+ await this.restoreUploads(selected);
+ }
+
+ this.cleanupRestoreModal();
+ }
+
+ async handleRestoreAll() {
+ const dialog = document.querySelector('dialog.restore-uploads');
+ if (!dialog) return;
+
+ const all = [];
+ dialog.querySelectorAll('.item.upload').forEach(item => {
+ all.push({
+ uploadId: item.dataset.uploadId,
+ fieldId: item.dataset.fieldId
+ });
+ });
+
+ await this.restoreUploads(all);
+ this.cleanupRestoreModal();
+ }
+
+ async restoreUploads(items) {
+ const byField = new Map();
+
+ items.forEach(item => {
+ if (!byField.has(item.fieldId)) {
+ byField.set(item.fieldId, []);
+ }
+ byField.get(item.fieldId).push(item.uploadId);
+ });
+
+ for (const [fieldId, uploadIds] of byField) {
+ await this.restoreFieldUploads(fieldId, uploadIds);
+ }
+ }
+
+ async restoreFieldUploads(fieldId, uploadIds) {
+ // Find or register field element
+ let fieldEl = this.fieldElements.get(fieldId);
+
+ if (!fieldEl) {
+ // Try to find by data attribute
+ const element = document.querySelector(`[data-uploader="${fieldId}"]`);
+ if (element) {
+ this.registerUploader(element);
+ fieldEl = this.fieldElements.get(fieldId);
+ }
+ }
+
+ if (!fieldEl) {
+ console.warn(`Field ${fieldId} not found for restoration`);
+ return;
+ }
+
+ // Show upload UI
+ if (fieldEl.ui.dropZone) fieldEl.ui.dropZone.hidden = true;
+ if (fieldEl.ui.groups?.display) fieldEl.ui.groups.display.hidden = false;
+
+ // Restore groups first
+ const groups = this.getFieldGroups(fieldId);
+ for (const group of groups) {
+ await this.restoreGroup(fieldId, group);
+ }
+
+ // Restore uploads
+ for (const uploadId of uploadIds) {
+ const upload = this.uploadStore.get(uploadId);
+ if (upload) {
+ await this.restoreUploadElement(fieldId, upload);
+ }
+ }
+
+ this.updateFieldState(fieldId);
+ this.refreshSortable(fieldId);
+ this.maybeLockUploads(fieldId);
+
+ // Auto-upload if configured
+ if (fieldEl.config.autoUpload && fieldEl.config.destination !== 'post_group') {
+ await this.queueUpload(fieldId);
+ }
+ }
+
+ async restoreGroup(fieldId, groupData) {
+ const group = await this.createGroup(fieldId, groupData.id);
+ if (!group) return;
+
+ // Restore field values
+ if (groupData.fields) {
+ const titleInput = group.element.querySelector('[name*="post_title"]');
+ const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+ if (titleInput && groupData.fields.post_title) {
+ titleInput.value = groupData.fields.post_title;
+ }
+ if (excerptInput && groupData.fields.post_excerpt) {
+ excerptInput.value = groupData.fields.post_excerpt;
+ }
+ }
+ }
+
+ async restoreUploadElement(fieldId, upload) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const file = await this.getBlobData(upload.id);
+ if (!file) return;
+
+ const preview = this.createPreviewUrl(file);
+ const element = this.createUploadElement({
+ id: upload.id,
+ preview,
+ meta: upload.meta,
+ subtype: this.getSubtypeFromMime(file.type)
+ }, fieldEl.config.destination === 'post_group');
+
+ // Determine location
+ let location;
+ if (upload.groupId) {
+ const groupEl = this.groupElements.get(upload.groupId);
+ location = groupEl?.grid || fieldEl.ui.preview;
+ } else {
+ location = fieldEl.ui.preview;
+ }
+
+ location.appendChild(element);
+ this.uploadElements.set(upload.id, { element, preview, location });
+
+ // Update upload status
+ upload.status = 'processed';
+ await this.uploadStore.save(upload);
+ }
+
+ handleClearCache() {
+ if (!confirm('Discard these uploads?')) return;
+ this.cleanupStoredData();
+ this.cleanupRestoreModal();
+ }
+
+ async cleanupStoredData() {
+ await this.uploadStore.clear();
+ await this.groupStore.clear();
+ }
+
+ cleanupRestoreModal() {
+ if (this.restoreModal) {
+ this.restoreModal.handleClose();
+ this.restoreSelection?.destroy();
+ this.restoreModal.destroy();
+ this.restoreModal.modal.remove();
+ this.restoreModal = null;
+ this.restoreSelection = null;
+ }
+ }
+
+ /*******************************************************************************
+ * HELPER METHODS
+ *******************************************************************************/
+
+ generateId(prefix) {
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ determineFieldId(fieldElement) {
+ const content = fieldElement.dataset.content ||
+ fieldElement.closest('dialog')?.dataset.content ||
+ fieldElement.closest('form')?.dataset.save || '';
+ const itemID = fieldElement.dataset.itemId ||
+ fieldElement.closest('dialog')?.dataset.itemId || '';
+ const field = fieldElement.dataset.field || '';
+
+ return `${content}_${itemID}_${field}`;
+ }
+
+ getFieldIdFromElement(el) {
+ const field = el.closest(this.selectors.field.field);
+ return field?.dataset.uploader || null;
+ }
+
+ getUploadIdFromElement(el) {
+ const item = el.closest(this.selectors.items.item);
+ return item?.dataset.uploadId || null;
+ }
+
+ getSubtypeFromMime(mimeType) {
+ if (mimeType.startsWith('image/')) return 'image';
+ if (mimeType.startsWith('video/')) return 'video';
+ return 'document';
+ }
+
+ getStatusIcon(status) {
+ return window.getIcon(this.queue.icons[status]);
+ }
+
+ getStatusProgress(status) {
+ const progress = {
+ 'processing': 28,
+ 'queued': 50,
+ 'uploading': 66,
+ 'pending': 75,
+ 'server_processing': 89,
+ 'completed': 100
+ };
+ return progress[status] || 0;
+ }
+
+ formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
+ }
+
+ createUploadElement(upload, draggable = false) {
+ const element = window.getTemplate('uploadItem');
+ if (!element) return null;
+
+ element.dataset.uploadId = upload.id;
+ element.dataset.subtype = upload.subtype || 'image';
+
+ const featured = element.querySelector('[name="featured"]');
+ const img = element.querySelector('img');
+ const video = element.querySelector('video');
+ const preview = element.querySelector('label > span');
+ const details = element.querySelector('details');
+
+ if (featured) featured.value = upload.id;
+
+ switch (upload.subtype) {
+ case 'image':
+ if (img) {
+ img.src = upload.preview;
+ img.alt = upload.meta?.originalName || '';
+ }
+ video?.remove();
+ preview?.remove();
+ break;
+ case 'video':
+ if (video) video.src = upload.preview;
+ img?.remove();
+ preview?.remove();
+ break;
+ case 'document':
+ const fileName = upload.meta?.originalName || '';
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
+ const iconMap = {
+ 'pdf': 'file-pdf', 'csv': 'file-csv',
+ 'doc': 'file-doc', 'docx': 'file-doc',
+ 'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+ };
+ const icon = window.getIcon(iconMap[ext] || 'file');
+ if (preview) {
+ preview.innerText = fileName;
+ preview.prepend(icon);
+ }
+ img?.remove();
+ video?.remove();
+ break;
+ }
+
+ if (details) {
+ const template = window.getTemplate('uploadMeta');
+ if (template) details.append(template);
+ }
+
+ element.draggable = draggable;
+
+ // Update input IDs
+ element.querySelectorAll('input').forEach(input => {
+ const id = input.id;
+ if (id) {
+ const newId = id + upload.id;
+ const label = input.parentNode.querySelector(`label[for="${id}"]`);
+ input.id = newId;
+ if (label) label.htmlFor = newId;
+ }
+ });
+
+ return element;
+ }
+
+ /**
+ * Get all files for a form
+ */
+ async getFilesForForm(formElement) {
+ const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+ const allFiles = [];
+
+ for (const field of uploadFields) {
+ const fieldId = this.determineFieldId(field);
+ const files = await this.getFilesForField(fieldId);
+ allFiles.push(...files);
+ }
+
+ return allFiles;
+ }
+
+ /**
+ * Get all files for a field
+ */
+ async getFilesForField(fieldId) {
+ const uploads = this.getFieldUploads(fieldId);
+ const files = [];
+
+ for (const upload of uploads) {
+ const file = await this.getBlobData(upload.id);
+ if (file) {
+ files.push({
+ file,
+ uploadId: upload.id,
+ fieldName: this.fieldElements.get(fieldId)?.config.name,
+ meta: upload.meta || {}
+ });
+ }
+ }
+
+ return files;
+ }
+
+ /*******************************************************************************
+ * EVENT SYSTEM
+ *******************************************************************************/
+
+ subscribe(callback) {
+ this.subscribers.add(callback);
+ return () => this.subscribers.delete(callback);
+ }
+
+ notify(event, data = {}) {
+ this.subscribers.forEach(cb => {
+ try { cb(event, data); } catch (e) { console.error('Subscriber error:', e); }
+ });
+ }
+
+ /*******************************************************************************
+ * DESTROY
+ *******************************************************************************/
+
+ destroy() {
+ document.removeEventListener('click', this.clickHandler);
+ document.removeEventListener('change', this.changeHandler);
+ document.removeEventListener('dragenter', this.dragEnterHandler);
+ document.removeEventListener('dragleave', this.dragLeaveHandler);
+ document.removeEventListener('dragover', this.dragOverHandler);
+ document.removeEventListener('drop', this.dropHandler);
+
+ this.selectionHandlers.forEach(handler => handler.destroy());
+ this.selectionHandlers.clear();
+
+ this.cleanupAllPreviewUrls();
+
+ this.sortableInstances.forEach(instance => instance?.destroy?.());
+ this.sortableInstances.clear();
+
+ this.uploadElements.clear();
+ this.fieldElements.clear();
+ this.groupElements.clear();
+ this.selected.clear();
+ this.subscribers.clear();
+ }
+}
+
+// Initialize
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ window.jvbUploads = new UploadManager();
+ }
+ });
+});
diff --git a/assets/js/concise/UploadManagerOld.js b/assets/js/concise/UploadManagerOld.js
new file mode 100644
index 0000000..3f1ae58
--- /dev/null
+++ b/assets/js/concise/UploadManagerOld.js
@@ -0,0 +1,3141 @@
+/**
+ * UploadManager - Refactored for clarity
+ *
+ * Architecture:
+ * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
+ * - Maps (uploadElements, fieldElements) = Runtime DOM references
+ * - Upload data flows: File → Process → Queue → Server → Clean up stores
+ */
+class UploadManager {
+ constructor() {
+ // Load dependencies
+ this.queue = window.jvbQueue;
+ this.a11y = window.jvbA11y;
+ this.error = window.jvbError;
+ this.fieldStoreReady = false;
+ this.uploadStoreReady = false;
+ this.hasCheckedForUploads = false;
+ const {fields, uploads} = window.jvbStore.register(
+ 'uploads',
+ [
+ {
+ storeName: 'fields',
+ keyPath: 'id',
+ indexes: [
+ { name: 'fieldId', keyPath: 'fieldId' },
+ { name: 'timestamp', keyPath: 'timestamp' },
+ { name: 'content', keyPath: 'content' },
+ { name: 'itemId', keyPath: 'itemId' },
+ { name: 'status', keyPath: 'status' }
+ ],
+ TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+ delayFetch: true
+ },
+ {
+ storeName: 'uploads',
+ keyPath: 'id',
+ storeBlobs: true,
+ indexes: [
+ { name: 'fieldId', keyPath: 'fieldId' },
+ { name: 'status', keyPath: 'status' },
+ { name: 'groupId', keyPath: 'groupId' },
+ { name: 'attachmentId', keyPath: 'attachmentId' }
+ ],
+ delayFetch: true
+ }
+ ]
+ );
+ this.fieldStore = fields;
+ this.uploadStore = uploads;
+
+ window.jvbUploadBlobs = this.uploadStore;
+
+ // Subscribe to store events
+ this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+ this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+ // RUNTIME DATA - DOM references and ephemeral state
+ this.uploadElements = new Map(); // uploadId → { element, preview, location }
+ this.fieldElements = new Map(); // fieldId → { element, ui, config }
+ this.groupElements = new Map(); // groupId → { element, grid, fieldId }
+
+ // Selection and UI state
+ this.selected = new Map();
+ this.selectionHandlers = new Map();
+ this.previewUrls = new Set();
+ this.sortableInstances = new Map();
+
+ // Worker for image processing
+ this.initWorker();
+
+ // Notification subscribers
+ this.subscribers = new Set();
+
+ // Selectors
+ this.selectors = {
+ field: {
+ field: '[data-upload-field]',
+ input: 'input[type="file"]',
+ dropZone: '.file-upload-container',
+ preview: '.item-grid.preview',
+ progress: '.image-progress'
+ },
+ groups: {
+ container: '.upload-group',
+ grid: '.item-grid.group',
+ header: '.group-header',
+ selectAll: '[name="select-all-group"]',
+ actions: '.group-actions',
+ count: '.selection-controls .info'
+ },
+ items: {
+ item: '[data-upload-id]',
+ checkbox: '[name*="select-item"]',
+ featured: '[name="featured"]',
+ details: 'details'
+ }
+ };
+
+ this.statusMapping = {
+ 'received': 'Image Received',
+ 'local_processing': 'Processing Image...',
+ 'queued': 'Waiting to upload...',
+ 'uploading': 'Uploading to Server',
+ 'pending': 'Successfully sent to server. In line for further processing.',
+ 'processing': 'Processing on server...',
+ 'completed': 'Upload complete!',
+ 'failed': 'Upload failed (will retry)',
+ 'failed_permanent': 'Upload failed permanently'
+ };
+
+ this.init();
+ }
+
+ async init() {
+ // this.initializeFields();
+ this.initListeners();
+
+ // Queue integration - handle completion/failure
+ this.queue.subscribe((event, operation) => {
+ if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
+ return;
+ }
+
+ const fieldId = operation.data instanceof FormData
+ ? operation.data.get('fieldId')
+ : operation.data?.fieldId;
+
+ switch(event) {
+ case 'cancel-operation':
+ if (fieldId) this.handleOperationCancelled(fieldId);
+ break;
+ case 'operation-status':
+ if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+ break;
+ case 'operation-complete':
+ this.handleOperationComplete(operation, fieldId);
+ break;
+ case 'operation-failed':
+ case 'operation-failed-permanent':
+ this.handleOperationFailed(operation, fieldId);
+ break;
+ }
+ });
+
+ window.addEventListener('beforeunload', () => {
+ this.cleanupAllPreviewUrls();
+ });
+ }
+
+ initWorker() {
+ this.worker = {
+ worker: null,
+ timeout: null,
+ tasks: new Map(),
+ restart: { count: 0, max: 3 },
+ settings: {
+ timeout: 10000,
+ batchSize: 1,
+ maxConcurrent: 3,
+ restartAfterTimeout: true
+ }
+ };
+ }
+
+ /*******************************************************************************
+ * FIELD MANAGEMENT
+ *******************************************************************************/
+ scanFields(container, autoUpload) {
+ console.log(autoUpload, 'autoUpload');
+ const fields = container.querySelectorAll(this.selectors.field.field);
+ fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
+ }
+
+ registerUploader(uploader, autoUpload) {
+ const fieldId = this.determineFieldId(uploader);
+ const config = this.extractFieldConfig(uploader, autoUpload);
+ const ui = this.buildFieldUI(uploader);
+
+ console.log(config, 'registering with config');
+ // Store field data with Sets for runtime
+ const fieldData = {
+ id: fieldId,
+ config: config,
+ uploads: new Set(),
+ groups: [],
+ state: 'ready',
+ timestamp: Date.now()
+ };
+
+ // Save to store (will convert Sets to Arrays automatically)
+ this.fieldStore.save(fieldData);
+
+ // Store DOM references separately
+ this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
+ uploader.dataset.uploader = fieldId;
+ this.addFieldSelectionHandler(fieldId);
+
+ if (config.type !== 'single') {
+ this.initSortable(fieldId);
+ }
+
+ return fieldId;
+ }
+
+ extractFieldConfig(fieldElement, autoUpload) {
+ return {
+ autoUpload: autoUpload,
+ destination: fieldElement.dataset.destination || 'meta',
+ content: fieldElement.dataset.content || null,
+ mode: fieldElement.dataset.mode || 'direct',
+ type: fieldElement.dataset.type || 'single',
+ name: fieldElement.dataset.field,
+ itemID: fieldElement.dataset.itemId || 0,
+ maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+ subtype: fieldElement.dataset.subtype || 'image'
+ };
+ }
+
+ buildFieldUI(fieldElement) {
+ let UI = {
+ field: fieldElement,
+ input: fieldElement.querySelector(this.selectors.field.input),
+ dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+ preview: fieldElement.querySelector(this.selectors.field.preview),
+ progress: {
+ progress: fieldElement.querySelector(this.selectors.field.progress),
+ bar: fieldElement.querySelector('.bar'),
+ fill: fieldElement.querySelector('.fill'),
+ details: fieldElement.querySelector('.details'),
+ text: fieldElement.querySelector('.details .text'),
+ count: fieldElement.querySelector('.details .count')
+ }
+ };
+
+ let display = fieldElement.querySelector('.group-display');
+ if (display) {
+ UI.groups = {
+ display: display,
+ container: fieldElement.querySelector('.item-grid.groups'),
+ empty: fieldElement.querySelector('.empty-group'),
+ groups: new Map()
+ };
+ }
+
+ return UI;
+ }
+
+ /*******************************************************************************
+ * SORTABLE INITIALIZATION
+ *******************************************************************************/
+ initSortable(fieldId) {
+ if (!window.Sortable) return;
+
+ // Mount MultiDrag plugin once
+ if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+ Sortable.mount(new Sortable.MultiDrag());
+ Sortable._multiDragMounted = true;
+ }
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ // Initialize sortable on all existing grids
+ const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+ grids.forEach(grid => {
+ const groupId = grid.classList.contains('group')
+ ? grid.closest('.upload-group')?.dataset.groupId
+ : null;
+ this.createSortableForGrid(grid, fieldId, groupId);
+ });
+
+ // Special handler for empty-group
+ const emptyGroup = fieldEl.element.querySelector('.empty-group');
+ if (emptyGroup && !emptyGroup.sortableInstance) {
+ emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+ animation: 150,
+ draggable: '.item',
+ multiDrag: true,
+ selectedClass: 'selected-for-drag',
+ avoidImplicitDeselect: true,
+ group: { name: fieldId, pull: false, put: true },
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+ onEnd: (evt) => this.handleDrop(evt, fieldId)
+ });
+ }
+ }
+
+ syncSortableSelection(fieldId, selectedItems) {
+ // Update Sortable's selection state to match checkboxes
+ this.sortableInstances.forEach((instance, key) => {
+ if (key.startsWith(fieldId)) {
+ const grid = instance.el;
+ const items = grid.querySelectorAll('.item');
+
+ items.forEach(item => {
+ const uploadId = item.dataset.uploadId;
+ const shouldBeSelected = selectedItems.has(uploadId);
+
+ if (shouldBeSelected) {
+ Sortable.utils.select(item);
+ } else {
+ Sortable.utils.deselect(item);
+ }
+ });
+ }
+ });
+ }
+
+ handleDrop(evt, fieldId) {
+ const dropTarget = evt.to;
+ const sourceTarget = evt.from;
+ const items = evt.items?.length > 0 ? evt.items : [evt.item];
+ const uploadIds = items.map(item => item.dataset.uploadId);
+
+ // Determine drop target type
+ const targetType = this.getDropTargetType(dropTarget);
+
+ switch (targetType) {
+ case 'empty-group':
+ this.handleDropToEmptyGroup(items, uploadIds, fieldId);
+ break;
+
+ case 'preview':
+ this.handleDropToPreview(items, uploadIds, fieldId);
+ break;
+
+ case 'group':
+ this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
+ break;
+ default:
+ // Fallback: return to preview
+ this.handleDropToPreview(items, uploadIds, fieldId);
+ break;
+ }
+
+ // Update UI
+ this.updateSortableState(dropTarget);
+ if (sourceTarget !== dropTarget) {
+ this.updateSortableState(sourceTarget);
+ }
+ }
+
+ /**
+ * Determine what type of drop target this is
+ */
+ getDropTargetType(target) {
+ if (target.classList.contains('empty-group')) {
+ return 'empty-group';
+ }
+
+ if (target.classList.contains('preview')) {
+ return 'preview';
+ }
+
+ if (target.classList.contains('group')) {
+ return 'group';
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * Handle drop to group: add to existing group
+ */
+ handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
+ try {
+ // If same container, it's just a reorder
+ if (dropTarget === sourceTarget) {
+ this.handleReorder({ to: dropTarget, items: items });
+ return;
+ }
+
+ // Moving to different group
+ uploadIds.forEach(uploadId => {
+ this.addToGroup(uploadId, dropTarget, false);
+ });
+
+ this.schedulePersistance(fieldId);
+
+ const message = items.length > 1
+ ? `Moved ${items.length} items to group`
+ : 'Moved item to group';
+ this.a11y.announce(message);
+
+ // Clear selection
+ const handler = this.selectionHandlers.get(fieldId);
+ handler?.clearSelection();
+ } catch (error) {
+ this.handleDropError(items, fieldId, error);
+ }
+ }
+
+ /**
+ * Handle drop to preview: remove from groups
+ */
+ handleDropToPreview(items, uploadIds, fieldId) {
+ try {
+ uploadIds.forEach(uploadId => {
+ this.removeFromGroup(uploadId);
+ });
+
+ this.schedulePersistance(fieldId);
+
+ const message = items.length > 1
+ ? `Moved ${items.length} items to preview`
+ : 'Moved item to preview';
+ this.a11y.announce(message);
+
+ // Clear selection
+ const handler = this.selectionHandlers.get(fieldId);
+ handler?.clearSelection();
+ } catch (error) {
+ this.handleDropError(items, fieldId, error);
+ }
+ }
+
+ /**
+ * Handle drop errors consistently
+ */
+ handleDropError(items, fieldId, error, message = 'An error occurred') {
+ console.error('Drop error:', error);
+
+ // Return items to preview as fallback
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (fieldEl?.ui?.preview) {
+ items.forEach(item => fieldEl.ui.preview.appendChild(item));
+ }
+
+ this.a11y.announce(`${message}. Items returned to preview.`);
+ }
+
+ /**
+ * Handle drop to group: add to existing group
+ */
+ handleDropToEmptyGroup(items, uploadIds, fieldId) {
+ try {
+ const group = this.createGroup(fieldId);
+ if (!group) {
+ this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
+ return;
+ }
+
+ // Move items to new group
+ items.forEach((item, index) => {
+ group.grid.appendChild(item);
+ this.addToGroup(uploadIds[index], group.grid, false);
+ });
+
+ this.schedulePersistance(fieldId);
+
+ const message = items.length > 1
+ ? `Created group with ${items.length} items`
+ : 'Created group with item';
+ this.a11y.announce(message);
+
+ // Clear selection after move
+ const handler = this.selectionHandlers.get(fieldId);
+ handler?.clearSelection();
+ } catch (error) {
+ this.handleDropError(items, fieldId, error);
+ }
+ }
+
+ /**
+ * Update sortable enabled/disabled state based on item count
+ */
+ updateSortableState(grid) {
+ const sortable = grid?.sortableInstance;
+ if (!sortable) return;
+
+ // const hasItems = grid.querySelectorAll('.item').length > 0;
+ sortable.option('disabled', false);
+ }
+
+ /**
+ * Refresh sortable for a field (call after adding/removing items dynamically)
+ */
+ refreshSortable(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl) return;
+
+ const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+ grids.forEach(grid => this.updateSortableState(grid));
+ }
+
+ handleReorder(evt) {
+ const grid = evt.to;
+ const fieldWrapper = grid.closest('.field, .upload');
+ if (!fieldWrapper) return;
+
+ // Get current order from DOM
+ let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+ .map(upload => upload.dataset.uploadId)
+ .filter(id => id);
+
+
+ // Update hidden input (for form submission)
+ let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+ if (hiddenInput && items.length > 0) {
+ hiddenInput.value = items.join(',');
+ }
+
+ // Update fieldState with new order
+ const fieldId = this.getFieldIdFromElement(grid);
+ if (fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+
+ // If reordering within a group, update that group's uploads array
+ if (grid.classList.contains('group')) {
+ const groupId = grid.dataset.groupId;
+ const group = fieldData?.groups?.find(g => g.id === groupId);
+ if (group) {
+ group.uploads = items; // Update order
+ }
+ }
+ // If reordering in preview, the order is implicit by DOM position
+ // (we don't store preview order separately)
+
+ this.schedulePersistance(fieldId);
+ }
+
+ this.a11y.announce('Item reordered');
+
+ fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+ detail: {
+ from: evt.from,
+ to: evt.to,
+ oldIndex: evt.oldIndex,
+ newIndex: evt.newIndex,
+ items: items
+ },
+ bubbles: true
+ }));
+ }
+
+ /*******************************************************************************
+ * FILE DROP HANDLERS
+ *******************************************************************************/
+
+ initListeners() {
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+
+ this.dragEnterHandler = this.handleExternalDragEnter.bind(this);
+ this.dragLeaveHandler = this.handleExternalDragLeave.bind(this);
+ this.dragOverHandler = this.handleExternalDragOver.bind(this);
+ this.dropHandler = this.handleExternalDrop.bind(this);
+
+ document.addEventListener('dragenter', this.dragEnterHandler);
+ document.addEventListener('dragleave', this.dragLeaveHandler);
+ document.addEventListener('dragover', this.dragOverHandler);
+ document.addEventListener('drop', this.dropHandler);
+ }
+
+ handleExternalDragLeave(e) {
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone && !dropZone.contains(e.relatedTarget)) {
+ dropZone.classList.remove('dragover');
+ }
+ }
+
+ handleExternalDragEnter(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ dropZone.classList.add('dragover');
+ }
+ }
+
+ handleExternalDragOver(e) {
+ if (!e.dataTransfer.types.includes('Files')) return;
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ }
+ }
+
+ handleExternalDrop(e) {
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (!dropZone) return;
+
+ e.preventDefault();
+ dropZone.classList.remove('dragover');
+
+ const files = Array.from(e.dataTransfer.files);
+ if (files.length === 0) return;
+
+ const fieldId = this.getFieldIdFromElement(dropZone);
+ if (fieldId) {
+ this.processFiles(fieldId, files);
+ this.a11y.announce(`${files.length} file(s) dropped for upload`);
+ }
+ }
+
+ /*******************************************************************************
+ * CLICK & CHANGE HANDLERS
+ *******************************************************************************/
+
+ handleClick(e) {
+ // Trigger file input
+ if (e.target.matches(this.selectors.field.dropZone) ||
+ e.target.closest(this.selectors.field.dropZone)) {
+ const dropZone = e.target.closest(this.selectors.field.dropZone);
+ if (dropZone && !e.target.matches('input, button, a')) {
+ const input = dropZone.querySelector(this.selectors.field.input);
+ input?.click();
+ }
+ }
+
+ // Group actions
+ const actionButton = e.target.closest('[data-action]');
+ if (actionButton) {
+ this.handleAction(actionButton);
+ }
+ }
+
+ handleChange(e) {
+ const fieldId = this.getFieldIdFromElement(e.target);
+
+ // File input change
+ if (e.target.matches(this.selectors.field.input)) {
+ const files = Array.from(e.target.files);
+ if (files.length > 0 && fieldId) {
+ this.processFiles(fieldId, files);
+ }
+ }
+
+ // Meta field changes
+ if (fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData.config.autoUpload) {
+ return;
+ }
+ if (fieldData?.config.destination === 'post_group') {
+ this.handleGroupMetaChange(e.target);
+ } else {
+ this.queueUploadMeta(e);
+ }
+ }
+ }
+
+ /*******************************************************************************
+ * FILE PROCESSING
+ *******************************************************************************/
+
+ async processFiles(fieldId, files) {
+ const fieldData = this.getFieldData(fieldId);
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldData || !fieldEl) return;
+
+ // Show group display, hide upload zone
+ if (fieldEl.ui.dropZone) {
+ fieldEl.ui.dropZone.hidden = true;
+ }
+ if (fieldEl.ui.groups?.display) {
+ fieldEl.ui.groups.display.hidden = false;
+ }
+
+ const totalFiles = files.length;
+ let processedCount = 0;
+
+ this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
+
+ const processPromises = Array.from(files).map(async (file) => {
+ try {
+ const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+ // Create initial upload data
+ const uploadData = {
+ id: uploadId,
+ attachmentId: null,
+ fieldId: fieldId,
+ status: 'local_processing',
+ groupId: null,
+ meta: {
+ originalName: file.name,
+ size: file.size,
+ type: file.type
+ }
+ };
+
+ // Save initial data
+ await this.uploadStore.save(uploadData);
+
+ // Process file
+ const preview = this.createPreviewUrl(file);
+ const processedFile = file.type.startsWith('image/')
+ ? await this.processImage(file, fieldData.config.subtype)
+ : file;
+
+ // Show progress
+ this.showUploadProgress(uploadId, true);
+ this.updateUploadItemProgress(uploadId, 50, 'local_processing');
+
+ // Store blob data (this updates the existing uploadData)
+ await this.saveBlobData(uploadId, processedFile || file);
+
+ // Create DOM element
+ const subtype = this.getSubtypeFromMime(file.type);
+ const element = this.createUploadElement({
+ id: uploadId,
+ preview: preview,
+ meta: uploadData.meta,
+ subtype: subtype
+ }, fieldData.config.destination === 'post_group');
+
+ // Add to preview grid
+ if (fieldEl.ui.preview) {
+ fieldEl.ui.preview.appendChild(element);
+
+ // Store runtime element data
+ this.uploadElements.set(uploadId, {
+ element: element,
+ preview: preview,
+ location: fieldEl.ui.preview
+ });
+ }
+
+ // Update status (gets existing data with blobData intact)
+ const storedUpload = this.uploadStore.get(uploadId);
+ if (storedUpload) {
+ storedUpload.status = 'processed';
+ await this.uploadStore.save(storedUpload);
+ }
+
+ // Add to field
+ fieldData.uploads.add(uploadId);
+ await this.saveFieldData(fieldData);
+
+ // Update progress
+ processedCount++;
+ this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+ this.updateUploadItemProgress(uploadId, 100, 'processed');
+
+ // Fade out progress
+ setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
+
+ return uploadId;
+
+ } catch (error) {
+ console.error('Error processing file:', file.name, error);
+ processedCount++;
+ this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
+ return null;
+ }
+ });
+
+ await Promise.all(processPromises);
+
+ this.updateFieldState(fieldId);
+ this.refreshSortable(fieldId);
+
+ // Queue for upload if in direct mode
+ if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
+ await this.queueUpload(fieldId);
+ this.maybeLockUploads(fieldId);
+ }
+ }
+
+ /*******************************************************************************
+ * IMAGE PROCESSING
+ *******************************************************************************/
+
+ async processImage(file, uploadId) {
+ const timeout = this.worker.settings.timeout;
+
+ return new Promise((resolve, reject) => {
+ let timeoutId;
+ let taskCompleted = false;
+
+ timeoutId = setTimeout(() => {
+ if (!taskCompleted) {
+ taskCompleted = true;
+ this.worker.tasks.delete(uploadId);
+ if (this.worker.settings.restartAfterTimeout) {
+ this.restartCompressionWorker();
+ }
+ reject(new Error(`Processing timeout for ${file.name}`));
+ }
+ }, timeout);
+
+ this.worker.tasks.set(uploadId, { file, timeoutId });
+
+ this.handleProcess(file, uploadId)
+ .then(result => {
+ if (!taskCompleted) {
+ taskCompleted = true;
+ clearTimeout(timeoutId);
+ this.worker.tasks.delete(uploadId);
+ resolve(result);
+ }
+ })
+ .catch(error => {
+ if (!taskCompleted) {
+ taskCompleted = true;
+ clearTimeout(timeoutId);
+ this.worker.tasks.delete(uploadId);
+ reject(error);
+ }
+ });
+ });
+ }
+
+ async handleProcess(file, uploadId) {
+ if (!file.type.startsWith('image/')) {
+ return file;
+ }
+
+ const maxDimension = this.getMaxDimension();
+ const quality = 0.85;
+
+ if (this.shouldUseWorker(file)) {
+ try {
+ if (!this.worker.worker) {
+ this.initCompressionWorker();
+ }
+ if (this.worker.worker) {
+ return await this.processWithWorker(file, uploadId, maxDimension, quality);
+ }
+ } catch (error) {
+ console.warn('Worker processing failed, falling back to main thread:', error);
+ }
+ }
+
+ return await this.processOnMainThread(file, maxDimension, quality);
+ }
+
+ async processOnMainThread(file, maxDimension, quality) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ let objectUrl = null;
+
+ const cleanup = () => {
+ img.onload = null;
+ img.onerror = null;
+ if (objectUrl) {
+ URL.revokeObjectURL(objectUrl);
+ objectUrl = null;
+ }
+ canvas.width = 1;
+ canvas.height = 1;
+ ctx.clearRect(0, 0, 1, 1);
+ };
+
+ img.onload = () => {
+ try {
+ const { width, height } = this.calculateOptimalDimensions(img, maxDimension);
+ canvas.width = width;
+ canvas.height = height;
+
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = 'high';
+ ctx.drawImage(img, 0, 0, width, height);
+
+ const outputFormat = this.getOptimalFormat(file);
+ const outputQuality = this.getOptimalQuality(file, quality);
+
+ canvas.toBlob(
+ (blob) => {
+ cleanup();
+ if (blob) {
+ const processedFile = new File(
+ [blob],
+ this.getProcessedFileName(file, outputFormat),
+ { type: outputFormat, lastModified: Date.now() }
+ );
+ resolve(processedFile);
+ } else {
+ reject(new Error('Canvas toBlob failed'));
+ }
+ },
+ outputFormat,
+ outputQuality
+ );
+
+ } catch (error) {
+ cleanup();
+ reject(new Error(`Canvas processing failed: ${error.message}`));
+ }
+ };
+
+ img.onerror = () => {
+ cleanup();
+ reject(new Error(`Failed to load image: ${file.name}`));
+ };
+
+ try {
+ objectUrl = this.createPreviewUrl(file);
+ img.src = objectUrl;
+ } catch (error) {
+ cleanup();
+ reject(new Error(`Failed to create object URL: ${error.message}`));
+ }
+ });
+ }
+
+ getOptimalFormat(file) {
+ if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
+ return file.type;
+ }
+ return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
+ }
+
+ getOptimalQuality(file, requestedQuality) {
+ if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
+ if (file.size < 2 * 1024 * 1024) return requestedQuality;
+ return Math.min(requestedQuality, 0.8);
+ }
+
+ getProcessedFileName(originalFile, outputFormat) {
+ const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
+ const extensions = {
+ 'image/webp': '.webp',
+ 'image/jpeg': '.jpg',
+ 'image/png': '.png',
+ 'image/gif': '.gif'
+ };
+ return baseName + (extensions[outputFormat] || '.jpg');
+ }
+
+ getMaxDimension() {
+ const screenWidth = window.screen.width;
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ if (screenWidth * devicePixelRatio > 2560) return 2400;
+ if (screenWidth * devicePixelRatio > 1920) return 1920;
+ return 1200;
+ }
+
+ shouldUseWorker(file) {
+ return this.worker.worker &&
+ file.size > 1024 * 1024 &&
+ typeof OffscreenCanvas !== 'undefined';
+ }
+
+ async processWithWorker(file, uploadId, maxDimension, quality) {
+ return new Promise((resolve, reject) => {
+ if (!this.worker.worker) {
+ reject(new Error('Worker not available'));
+ return;
+ }
+
+ const messageId = `${uploadId}_${Date.now()}`;
+
+ const messageHandler = (e) => {
+ if (e.data.messageId !== messageId) return;
+
+ this.worker.worker.removeEventListener('message', messageHandler);
+ this.worker.worker.removeEventListener('error', errorHandler);
+
+ if (e.data.success) {
+ const processedFile = new File(
+ [e.data.blob],
+ this.getProcessedFileName(file, e.data.format || 'image/webp'),
+ { type: e.data.format || 'image/webp', lastModified: Date.now() }
+ );
+ resolve(processedFile);
+ } else {
+ reject(new Error(e.data.error || 'Worker processing failed'));
+ }
+ };
+
+ const errorHandler = (error) => {
+ this.worker.worker.removeEventListener('message', messageHandler);
+ this.worker.worker.removeEventListener('error', errorHandler);
+ reject(new Error(`Worker error: ${error.message}`));
+ };
+
+ this.worker.worker.addEventListener('message', messageHandler);
+ this.worker.worker.addEventListener('error', errorHandler);
+
+ this.worker.worker.postMessage({
+ messageId,
+ file,
+ maxDimension,
+ quality,
+ outputFormat: this.getOptimalFormat(file)
+ });
+ });
+ }
+
+ restartCompressionWorker() {
+ if (this.worker.worker) {
+ this.worker.worker.terminate();
+ this.worker.worker = null;
+ }
+ this.worker.tasks.clear();
+ if (this.worker.restart.count >= this.worker.restart.max) {
+ console.error('Max worker restarts reached, disabling worker');
+ return;
+ }
+ this.worker.restart.count++;
+ this.initCompressionWorker();
+ }
+
+ initCompressionWorker() {
+ if (this.worker.worker || typeof Worker === 'undefined') return;
+
+ try {
+ const workerScript = `
+ self.onmessage = async function(e) {
+ const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+ try {
+ const bitmap = await createImageBitmap(file);
+ const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+ const width = Math.round(bitmap.width * scale);
+ const height = Math.round(bitmap.height * scale);
+ const canvas = new OffscreenCanvas(width, height);
+ const ctx = canvas.getContext('2d');
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = 'high';
+ ctx.drawImage(bitmap, 0, 0, width, height);
+ bitmap.close();
+ const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });
+ self.postMessage({ messageId, success: true, blob: blob, format: outputFormat });
+ } catch (error) {
+ self.postMessage({ messageId, success: false, error: error.message });
+ }
+ };
+ `;
+
+ const blob = new Blob([workerScript], { type: 'application/javascript' });
+ this.worker.worker = new Worker(this.createPreviewUrl(blob));
+
+ } catch (error) {
+ console.warn('Failed to initialize compression worker:', error);
+ this.worker.worker = null;
+ }
+ }
+
+ calculateOptimalDimensions(img, maxDimension) {
+ let { width, height } = img;
+ if (width <= maxDimension && height <= maxDimension) {
+ return { width, height };
+ }
+ const scale = Math.min(maxDimension / width, maxDimension / height);
+ return {
+ width: Math.round(width * scale),
+ height: Math.round(height * scale)
+ };
+ }
+
+ supportsWebP() {
+ const canvas = document.createElement('canvas');
+ return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
+ }
+
+ createPreviewUrl(file) {
+ const url = URL.createObjectURL(file);
+ if (!this.previewUrls) this.previewUrls = new Set();
+ this.previewUrls.add(url);
+ return url;
+ }
+
+ revokePreviewUrl(url) {
+ if (url?.startsWith('blob:')) {
+ URL.revokeObjectURL(url);
+ this.previewUrls?.delete(url);
+ }
+ }
+
+ /*******************************************************************************
+ * QUEUE INTEGRATION
+ *******************************************************************************/
+
+ async submitUploads(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldData?.uploads || fieldData.uploads.size === 0) {
+ return;
+ }
+
+ let uploadIds = Array.from(fieldData.uploads);
+ if (uploadIds.length === 0) {
+ this.error.log('No uploads to upload', {
+ component: 'UploadManager',
+ action: 'submitGroupedUploads',
+ fieldId: fieldId
+ });
+ return;
+ }
+
+ const fieldGroups = this.getFieldGroups(fieldId);
+
+ if (fieldGroups.length === 0) {
+ this.error.log('No groups created for post_group upload', {
+ component: 'UploadManager',
+ action: 'submitGroupedUploads',
+ fieldId: fieldId
+ });
+ return;
+ }
+
+ // Build posts array from groups
+ const posts = [];
+ const formData = new FormData();
+ let uploadMap = [];
+
+ // Process each group
+ for (const group of fieldGroups) {
+ const post = {
+ images: [],
+ fields: {}
+ };
+
+ // Add group metadata
+ for (let [name, value] of Object.entries(group.changes)) {
+ post.fields[name] = value;
+ }
+
+ // Get uploads for this group
+ const groupUploadIds = uploadIds.filter(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ return upload?.groupId === group.id;
+ });
+
+ // Add files for this group
+ for (const uploadId of groupUploadIds) {
+ const file = await this.getBlobData(uploadId);
+ if (file) {
+ formData.append('files[]', file);
+
+ const imageData = {
+ upload_id: uploadId,
+ index: uploadMap.length
+ };
+
+ // Check if featured
+ const uploadEl = this.uploadElements.get(uploadId);
+ const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
+ if (radioInput?.checked) {
+ post.fields.featured = uploadId;
+ }
+
+ post.images.push(imageData);
+ uploadMap.push(uploadId);
+ }
+ }
+
+ posts.push(post);
+ }
+
+ // Handle remaining uploads (without groupId) - each becomes its own post
+ const remainingUploadIds = uploadIds.filter(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ return !upload?.groupId;
+ });
+
+ for (const uploadId of remainingUploadIds) {
+ const post = {
+ images: [],
+ fields: {}
+ };
+
+ const file = await this.getBlobData(uploadId);
+ if (file) {
+ formData.append('files[]', file);
+
+ const imageData = {
+ upload_id: uploadId,
+ index: uploadMap.length
+ };
+ post.images.push(imageData);
+ uploadMap.push(uploadId);
+ }
+
+ posts.push(post);
+ }
+
+ // Add metadata to FormData
+ formData.append('content', fieldData.config.content);
+ formData.append('user', fieldData.config.itemID);
+ formData.append('posts', JSON.stringify(posts));
+ formData.append('upload_ids', JSON.stringify(uploadMap));
+
+ const operation = {
+ endpoint: 'uploads/groups',
+ method: 'POST',
+ data: formData,
+ title: `Creating ${posts.length} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+ popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+ canMerge: false,
+ headers: {
+ 'action_nonce': window.auth.getNonce('dash')
+ },
+ append: '_upload',
+ };
+
+ try {
+ const operationId = await this.queue.addToQueue(operation);
+
+ // Update upload statuses
+ uploadIds.forEach(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ if (upload) {
+ upload.operationId = operationId;
+ upload.status = 'queued';
+ this.uploadStore.save(upload);
+ this.updateUploadStatus(uploadId, 'queued');
+ }
+ });
+
+ fieldData.operationId = operationId;
+ await this.saveFieldData(fieldData);
+
+ this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
+
+ return operationId;
+ } catch (error) {
+ this.error.log(error, {
+ component: 'UploadManager',
+ action: 'submitGroupedUploads',
+ fieldId: fieldId
+ });
+ throw error;
+ }
+ }
+
+ async queueUpload(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
+
+ const uploads = Array.from(fieldData.uploads);
+ const data = this.prepareUploadData(fieldData, uploads);
+
+ this.a11y.announce('Queuing for upload');
+
+ const operation = {
+ endpoint: 'uploads',
+ method: 'POST',
+ data: data,
+ title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
+ popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+ canMerge: false,
+ headers: { 'action_nonce': window.auth.getNonce('dash') },
+ append: '_upload'
+ };
+
+ try {
+ const operationId = await this.queue.addToQueue(operation);
+
+ // Update upload statuses
+ uploads.forEach(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ if (upload) {
+ upload.operationId = operationId;
+ upload.status = 'queued';
+ this.uploadStore.save(upload);
+ this.updateUploadStatus(uploadId, 'queued');
+ }
+ });
+
+ fieldData.operationId = operationId;
+ await this.saveFieldData(fieldData);
+
+ return operationId;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async prepareUploadData(fieldData, uploads) {
+ const formData = new FormData();
+ formData.append('content', fieldData.config.content);
+ formData.append('mode', fieldData.config.mode);
+ formData.append('field_name', fieldData.config.name);
+ formData.append('fieldId', fieldData.id);
+ formData.append('field_type', fieldData.config.type);
+ formData.append('subtype', fieldData.config.subtype);
+ formData.append('item_id', fieldData.config.itemID);
+ formData.append('destination', fieldData.config.destination || 'meta');
+
+ let uploadMap = [];
+
+
+ const blobPromises = uploads.map(async (uploadId) => {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) return;
+
+ const file = await this.getBlobData(uploadId);
+ if (file) {
+ formData.append('files[]', file);
+ uploadMap.push(upload.id);
+ }
+ });
+
+ await Promise.all(blobPromises);
+
+ formData.append('upload_ids', JSON.stringify(uploadMap));
+ return formData;
+ }
+
+ async queueUploadMeta(e) {
+ const uploadId = this.getUploadIdFromElement(e.target);
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) return;
+
+ const fieldData = this.getFieldData(upload.fieldId);
+ if (!fieldData) return;
+
+ let data = {};
+ data[e.target.name] = e.target.value;
+
+ upload.meta = { ...upload.meta, ...data };
+ await this.uploadStore.save(upload);
+
+ let queueData = {};
+ queueData[upload.attachmentId ?? upload.id] = upload.meta;
+
+ const operation = {
+ endpoint: 'uploads/meta',
+ method: 'POST',
+ data: queueData,
+ title: 'Updating meta',
+ canMerge: true,
+ headers: { 'action_nonce': window.auth.getNonce('dash') }
+ };
+
+ try {
+ await this.queue.addToQueue(operation);
+ } catch (error) {
+ this.error.log(error, {
+ component: 'UploadManager',
+ action: 'sendMetaUpdate',
+ uploadId: upload.id
+ });
+ }
+ }
+
+ /*******************************************************************************
+ * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
+ *******************************************************************************/
+
+ /**
+ * Handle successful operation completion - CLEAR STORES
+ */
+ async handleOperationComplete(operation, fieldId) {
+ const results = operation.result?.data || operation.serverData?.data || [];
+
+ // Update upload statuses with attachment IDs
+ results.forEach(result => {
+ const upload = this.uploadStore.get(result.upload_id);
+ if (upload) {
+ upload.attachmentId = result.attachment_id;
+ upload.status = 'completed';
+ this.uploadStore.save(upload);
+ this.updateUploadStatus(result.upload_id, 'completed');
+ }
+ });
+
+ if (!fieldId) return;
+
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData) return;
+
+ // Clean up completed uploads from stores
+ const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ return upload?.status === 'completed';
+ });
+
+ for (const uploadId of completedUploads) {
+ await this.clearUpload(uploadId, false);
+ fieldData.uploads.delete(uploadId);
+ }
+
+ // If all uploads complete, clear entire field from stores
+ if (fieldData.uploads.size === 0) {
+ await this.clearFieldFromStores(fieldId);
+ this.a11y.announce('All uploads completed successfully');
+ } else {
+ // Otherwise just update field state
+ await this.saveFieldData(fieldData);
+ }
+
+ this.updateFieldState(fieldId);
+ }
+
+ /**
+ * Handle operation failure
+ */
+ handleOperationFailed(operation, fieldId) {
+ const uploadIds = operation.data instanceof FormData
+ ? JSON.parse(operation.data.get('upload_ids') || '[]')
+ : operation.data.upload_ids || [];
+
+ uploadIds.forEach(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ if (upload) {
+ upload.status = operation.status === 'operation-failed-permanent'
+ ? 'failed_permanent'
+ : 'failed';
+ this.uploadStore.save(upload);
+ this.updateUploadStatus(uploadId, upload.status);
+ }
+ });
+
+ if (fieldId) {
+ this.updateFieldState(fieldId);
+ }
+ }
+
+ /**
+ * Handle operation cancellation
+ */
+ async handleOperationCancelled(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData) return;
+
+ const uploadsArray = fieldData.uploads instanceof Set
+ ? Array.from(fieldData.uploads)
+ : fieldData.uploads;
+
+ for (const uploadId of uploadsArray) {
+ await this.clearUpload(uploadId, false);
+ }
+
+ await this.clearFieldFromStores(fieldId);
+ this.updateFieldState(fieldId);
+ this.a11y.announce('Upload cancelled');
+ }
+
+ getFieldGroups(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData?.groups) return [];
+
+ return fieldData.groups.map(group => ({
+ id: group.id,
+ uploads: group.uploads || [],
+ changes: group.changes || {}
+ }));
+ }
+
+ getSelectedRestorationUploads(notificationEl) {
+ let selected = [];
+ const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+ checkboxes.forEach(checkbox => {
+ const item = checkbox.closest('.item');
+ if (item) {
+ selected.push({
+ uploadId: item.dataset.uploadId,
+ fieldId: item.dataset.fieldId
+ });
+ }
+ });
+
+ return selected;
+ }
+
+ async restoreSelectedUploads(selectedUploads) {
+ const byField = new Map();
+ selectedUploads.forEach(item => {
+ if (!byField.has(item.fieldId)) {
+ byField.set(item.fieldId, []);
+ }
+ byField.get(item.fieldId).push(item.uploadId);
+ });
+
+ for (const [fieldId, uploadIds] of byField.entries()) {
+ const fieldState = this.fieldStore.get(fieldId);
+ if (fieldState) {
+ fieldState.uploads = uploadIds;
+ await this.restoreField(fieldState);
+ }
+ }
+ }
+
+ async restoreField(fieldState) {
+ const { config, context, uploads, groups, id } = fieldState;
+
+ // If in a modal, open it first
+ if (context?.modalType) {
+ await this.openModalForRestore(context);
+ }
+
+ // Find field element
+ let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+ if (!fieldElement) {
+ const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+ fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+ }
+
+ if (!fieldElement) {
+ console.warn(`Field ${config.name} not found for restoration`, config);
+ return;
+ }
+
+ // Register the field if not already registered
+ let fieldKey = fieldElement.dataset.uploader;
+ if (!fieldKey || !this.fieldElements.has(fieldKey)) {
+ fieldKey = this.registerUploader(fieldElement);
+ }
+
+ const fieldEl = this.fieldElements.get(fieldKey);
+ const fieldData = this.getFieldData(fieldKey);
+
+ if (!fieldEl || !fieldData) {
+ console.error('Failed to register field for restoration');
+ return;
+ }
+
+ // Merge saved state back into field
+ fieldData.state = fieldState.state || 'ready';
+
+ // Rebuild UI references if needed
+ if (!fieldEl.ui) {
+ fieldEl.ui = this.buildFieldUI(fieldElement);
+ }
+
+ if (fieldEl.ui.groups?.display) {
+ fieldEl.ui.groups.display.hidden = false;
+ }
+ if (fieldEl.ui.dropZone) {
+ fieldEl.ui.dropZone.hidden = true;
+ }
+
+ // Restore groups first
+ if (groups && groups.length > 0) {
+ await this.restoreGroups(fieldKey, groups);
+ }
+
+ // Handle both Array and Set for uploads
+ const uploadsArray = uploads instanceof Set
+ ? Array.from(uploads)
+ : Array.isArray(uploads)
+ ? uploads
+ : [];
+
+ // Restore uploads
+ for (const uploadId of uploadsArray) {
+ // Get upload data from store
+ const uploadData = this.uploadStore.get(uploadId);
+ if (uploadData) {
+ await this.restoreUpload(fieldKey, uploadData);
+ }
+ }
+
+ // Update field state
+ await this.saveFieldData(fieldData);
+ this.updateFieldState(fieldKey);
+ this.maybeLockUploads(fieldKey);
+ this.refreshSortable(fieldKey);
+
+ // Queue for upload if needed
+ console.log(config);
+ if (config.autoUpload && config.mode === 'direct' && config.destination !== 'post_group') {
+ await this.queueUpload(fieldKey);
+ }
+ }
+
+ async restoreUpload(fieldId, uploadData) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ const fieldData = this.getFieldData(fieldId);
+
+ if (!fieldEl || !fieldData) {
+ console.error('Field not found for upload restoration:', fieldId);
+ return;
+ }
+
+ // Get reconstructed File from blob data
+ const file = await this.getBlobData(uploadData.id);
+
+ if (!file) {
+ console.warn('Blob data not found for upload:', uploadData.id);
+ return;
+ }
+
+ // Create preview URL
+ const previewUrl = this.createPreviewUrl(file);
+
+ // Recreate DOM element
+ const subtype = this.getSubtypeFromMime(file.type);
+ const element = this.createUploadElement({
+ id: uploadData.id,
+ preview: previewUrl,
+ meta: uploadData.meta || {
+ originalName: file.name,
+ size: file.size,
+ type: file.type
+ },
+ subtype: subtype
+ }, fieldData.config.destination === 'post_group');
+
+ // Determine correct location
+ let location;
+ if (uploadData.groupId) {
+ // Check if group exists
+ const groupEl = this.groupElements.get(uploadData.groupId);
+ if (groupEl?.grid) {
+ location = groupEl.grid;
+
+ // Add to group's upload list
+ const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
+ if (group) {
+ if (!group.uploads) group.uploads = [];
+ if (!group.uploads.includes(uploadData.id)) {
+ group.uploads.push(uploadData.id);
+ }
+ }
+ } else {
+ // Group doesn't exist, add to preview
+ location = fieldEl.ui.preview;
+ uploadData.groupId = null;
+ }
+ } else {
+ // No group, add to preview
+ location = fieldEl.ui.preview;
+ }
+
+ // Add element to DOM
+ if (location) {
+ location.appendChild(element);
+ } else if (fieldEl.ui.preview) {
+ fieldEl.ui.preview.appendChild(element);
+ location = fieldEl.ui.preview;
+ }
+
+ // Store runtime element data
+ this.uploadElements.set(uploadData.id, {
+ element: element,
+ preview: previewUrl,
+ location: location
+ });
+
+ // Add to field uploads
+ if (!fieldData.uploads) fieldData.uploads = new Set();
+ fieldData.uploads.add(uploadData.id);
+
+ // Update upload data in store
+ uploadData.status = 'processed';
+ await this.uploadStore.save(uploadData);
+
+ // Update sortable state for the grid
+ if (location) {
+ this.updateSortableState(location);
+ }
+ }
+
+ async restoreGroups(fieldId, groups) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ const fieldData = this.getFieldData(fieldId);
+
+ if (!fieldEl || !fieldData) {
+ console.error('Field not found for group restoration:', fieldId);
+ return;
+ }
+
+ for (const groupData of groups) {
+ const group = this.createGroup(fieldId, groupData.id);
+ if (!group) {
+ console.warn('Failed to create group:', groupData.id);
+ continue;
+ }
+
+ const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
+ if (storedGroup) {
+ // Restore metadata
+ if (groupData.changes) {
+ storedGroup.changes = { ...groupData.changes };
+ }
+
+ // Preserve upload order
+ if (groupData.uploads) {
+ storedGroup.uploads = [...groupData.uploads];
+ }
+
+ // Restore form field values
+ if (groupData.changes) {
+ const titleInput = group.element.querySelector('[name*="post_title"]');
+ const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+ if (titleInput && groupData.changes.post_title) {
+ titleInput.value = groupData.changes.post_title;
+ }
+ if (excerptInput && groupData.changes.post_excerpt) {
+ excerptInput.value = groupData.changes.post_excerpt;
+ }
+ }
+ }
+ }
+
+ await this.saveFieldData(fieldData);
+ }
+
+ async openModalForRestore(context) {
+ if (!context) return;
+
+ const { modalType, itemId } = context;
+
+ // Find and click the appropriate button to open the modal
+ let trigger = null;
+
+ switch(modalType) {
+ case 'create':
+ trigger = document.querySelector('[data-action="create"]');
+ break;
+ case 'edit':
+ // Need to find the specific edit button
+ if (itemId) {
+ trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
+ }
+ break;
+ case 'bulkEdit':
+ trigger = document.querySelector('[data-action="bulk-edit"]');
+ break;
+ }
+
+ if (trigger) {
+ trigger.click();
+
+ // Wait for modal to open and render
+ await new Promise(resolve => setTimeout(resolve, 300));
+ } else {
+ console.warn('Modal trigger not found for restoration:', context);
+ }
+ }
+
+ formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+ }
+
+ /*******************************************************************************
+ * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
+ *******************************************************************************/
+
+ /**
+ * Clear individual upload from stores (called after successful upload)
+ */
+ async clearUpload(uploadId, persist = true) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (uploadEl) {
+ this.revokePreviewUrl(uploadEl.preview);
+ if (uploadEl.element) {
+ const previewUrl = uploadEl.element.dataset.previewUrl;
+ this.revokePreviewUrl(previewUrl);
+ delete uploadEl.element.dataset.previewUrl;
+ }
+ }
+
+ // Remove from runtime memory
+ this.uploadElements.delete(uploadId);
+
+ // Remove from store (no separate blob store - it's part of the upload object)
+ await this.uploadStore.delete(uploadId);
+
+ // Update field if needed
+ if (persist) {
+ const upload = this.uploadStore.get(uploadId);
+ if (upload?.fieldId) {
+ await this.schedulePersistance(upload.fieldId);
+ }
+ }
+ }
+
+ /**
+ * Clear entire field from stores (called when all uploads complete)
+ */
+ async clearFieldFromStores(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+
+ // Clear all related uploads
+ if (fieldData?.uploads) {
+ const uploadsArray = fieldData.uploads instanceof Set
+ ? Array.from(fieldData.uploads)
+ : fieldData.uploads;
+
+ for (const uploadId of uploadsArray) {
+ await this.uploadStore.delete(uploadId);
+ }
+ }
+
+ // Clear field from store
+ await this.fieldStore.delete(fieldId);
+
+ // Keep runtime references (fieldElements, etc) intact for reuse
+ }
+
+ cleanupAllPreviewUrls() {
+ if (this.previewUrls) {
+ this.previewUrls.forEach(url => {
+ try {
+ URL.revokeObjectURL(url);
+ } catch (e) {
+ // Ignore errors during cleanup
+ }
+ });
+ this.previewUrls.clear();
+ }
+ }
+
+ /*******************************************************************************
+ * UI UPDATE METHODS
+ *******************************************************************************/
+
+ updateFieldState(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldEl || !fieldData) return;
+
+ const container = fieldEl.element;
+ const uploadCount = fieldData.uploads?.size || 0;
+ const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+ container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+ container.dataset.uploadCount = uploadCount.toString();
+ container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+ if (fieldEl.ui.preview) {
+ fieldEl.ui.preview.setAttribute('aria-label',
+ `Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+ );
+ }
+ }
+
+ updateUploadProgress(fieldId, current, total, message) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl?.ui?.progress?.progress) return;
+
+ const progress = fieldEl.ui.progress;
+ const percent = total > 0 ? (current / total) * 100 : 0;
+
+ if (progress.fill) progress.fill.style.width = `${percent}%`;
+ if (progress.text) progress.text.textContent = message;
+ if (progress.count) progress.count.textContent = `${current}/${total}`;
+
+ progress.progress.hidden = (current === total);
+ }
+
+ updateFieldStatus(fieldId, status) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData) return;
+
+ fieldData.state = status;
+ this.saveFieldData(fieldData);
+ }
+
+ updateUploadStatus(uploadId, status) {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) return;
+
+ upload.status = status;
+ this.uploadStore.save(upload);
+ this.updateUploadUI(uploadId);
+ }
+
+ updateUploadUI(uploadId) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload || !uploadEl?.element) return;
+
+ uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+ uploadEl.element.classList.add(`status-${upload.status}`);
+
+ const progress = uploadEl.element.querySelector('.progress');
+ if (progress) {
+ this.updateUploadItemProgress(uploadId,
+ this.getStatusProgress(upload.status),
+ upload.status
+ );
+ }
+ }
+
+ showUploadProgress(uploadId, show = true) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!uploadEl?.element) return;
+
+ const progressEl = uploadEl.element.querySelector('.progress');
+ if (progressEl) {
+ if (show) {
+ progressEl.style.removeProperty('animation');
+ progressEl.hidden = false;
+ } else {
+ progressEl.style.animation = 'fadeOut var(--transition-base)';
+ setTimeout(() => { progressEl.hidden = true; }, 300);
+ }
+ }
+ }
+
+ updateUploadItemProgress(uploadId, percent, status = null) {
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!uploadEl?.element) return;
+
+ const progressEl = uploadEl.element.querySelector('.progress');
+ if (!progressEl) return;
+
+ const fill = progressEl.querySelector('.fill');
+ const details = progressEl.querySelector('.details');
+ const icon = progressEl.querySelector('.icon');
+
+ if (fill) fill.style.width = `${percent}%`;
+ if (status && details) details.textContent = this.getStatusText(status);
+ if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+ }
+
+ maybeLockUploads(fieldId) {
+ const fieldEl = this.fieldElements.get(fieldId);
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldEl?.ui?.dropZone || !fieldData) return;
+
+ const uploadCount = fieldData.uploads?.size || 0;
+
+ // For groupable uploads, set max to 20
+ const maxFiles = fieldData.config.destination === 'post_group'
+ ? 20
+ : (fieldData.config?.maxFiles || 999);
+
+ fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+ fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+ // Show helpful message for groupable uploads
+ if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
+ this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
+ }
+ }
+
+ /*******************************************************************************
+ * GROUP MANAGEMENT
+ *******************************************************************************/
+ /**
+ * Create sortable instance for a grid
+ */
+ createSortableForGrid(grid, fieldId, groupId = null) {
+ if (!grid || grid.sortableInstance) return;
+
+ const sortableInstance = new Sortable(grid, {
+ animation: 150,
+ draggable: '.item',
+ multiDrag: true,
+ selectedClass: 'selected-for-drag',
+ avoidImplicitDeselect: true,
+ group: { name: fieldId, pull: true, put: true },
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+
+ // Centralized drop handler
+ onEnd: (evt) => this.handleDrop(evt, fieldId),
+
+ // Selection sync
+ onSelect: (evt) => {
+ const checkbox = evt.item.querySelector('[name*="select-item"]');
+ if (checkbox && !checkbox.checked) {
+ checkbox.checked = true;
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ },
+
+ onDeselect: (evt) => {
+ const checkbox = evt.item.querySelector('[name*="select-item"]');
+ if (checkbox && checkbox.checked) {
+ checkbox.checked = false;
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ },
+
+ onAdd: (evt) => this.updateSortableState(evt.to),
+ onRemove: (evt) => this.updateSortableState(evt.from)
+ });
+
+ grid.sortableInstance = sortableInstance;
+
+ const gridId = groupId
+ ? `${fieldId}-group-${groupId}`
+ : `${fieldId}-preview`;
+
+ this.sortableInstances.set(gridId, sortableInstance);
+
+ return sortableInstance;
+ }
+ createGroup(fieldId, groupId = null) {
+ const fieldData = this.getFieldData(fieldId);
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldData || !fieldEl) return null;
+
+ if (!groupId) {
+ groupId = `group_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+ }
+
+ const groupElement = this.createGroupElement(groupId, fieldId);
+ if (!groupElement) return null;
+
+ // Store in field UI Map
+ if (!fieldEl.ui.groups) {
+ fieldEl.ui.groups = {
+ groups: new Map(),
+ container: null,
+ empty: null,
+ display: null
+ };
+ }
+
+ fieldEl.ui.groups.groups.set(groupId, groupElement);
+
+ // Insert into DOM
+ if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
+ fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
+ } else if (fieldEl.ui.groups.container) {
+ fieldEl.ui.groups.container.appendChild(groupElement);
+ }
+
+ // Store group element reference
+ const grid = groupElement.querySelector('.item-grid.group');
+ this.groupElements.set(groupId, {
+ element: groupElement,
+ grid: grid,
+ fieldId: fieldId
+ });
+
+ // Add to field groups
+ if (!fieldData.groups) fieldData.groups = [];
+ const existingGroup = fieldData.groups.find(g => g.id === groupId);
+ if (!existingGroup) {
+ fieldData.groups.push({
+ id: groupId,
+ uploads: [],
+ changes: {}
+ });
+ this.saveFieldData(fieldData);
+ }
+
+ // Initialize selection handler
+ this.addGroupSelectionHandler(fieldId, groupId);
+
+ if (grid) {
+ this.createSortableForGrid(grid, fieldId, groupId);
+ }
+
+ return { id: groupId, element: groupElement, grid: grid };
+ }
+ createGroupElement(groupId, fieldId) {
+ let groupElement = window.getTemplate('imageGroup');
+ if (!groupElement) return;
+
+ groupElement.dataset.groupId = groupId;
+ groupElement.dataset.fieldId = fieldId;
+
+ let fields = window.getTemplate('groupMetadata');
+ const fieldsContainer = groupElement.querySelector('.fields');
+ if (fieldsContainer && fields) {
+ fieldsContainer.append(fields);
+
+ const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+ const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+ if (titleInput) {
+ titleInput.id = `${groupId}_title`;
+ titleInput.name = `${groupId}[post_title]`;
+ }
+ if (excerptInput) {
+ excerptInput.id = `${groupId}_excerpt`;
+ excerptInput.name = `${groupId}[post_excerpt]`;
+ }
+
+ const fieldData = this.getFieldData(fieldId);
+ if (fieldData && fieldData.config.content !== '') {
+ let summary = groupElement.querySelector('summary');
+ if (summary) summary.textContent = fieldData.config.content + ' Fields';
+ }
+ } else {
+ groupElement.querySelector('details')?.remove();
+ }
+
+ const gridContainer = groupElement.querySelector('.item-grid.group');
+ if (gridContainer) {
+ gridContainer.dataset.groupId = groupId;
+ }
+
+ return groupElement;
+ }
+
+ deleteGroup(groupId, confirm = true) {
+ const groupEl = this.groupElements.get(groupId);
+ if (!groupEl) return;
+
+ const fieldData = this.getFieldData(groupEl.fieldId);
+ if (!fieldData) return;
+
+ const group = fieldData.groups?.find(g => g.id === groupId);
+ let keepUploads = true;
+
+ if (confirm && group?.uploads?.length > 0) {
+ keepUploads = !window.confirm('Delete uploads in group?');
+ }
+
+ if (confirm && keepUploads && group?.uploads) {
+ // Move uploads back to preview
+ group.uploads.forEach(uploadId => {
+ this.removeFromGroup(uploadId);
+ });
+ }
+
+ // Remove from field groups
+ if (fieldData.groups) {
+ fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
+ this.saveFieldData(fieldData);
+ }
+
+ // Remove DOM element
+ if (groupEl.element) {
+ groupEl.element.remove();
+ this.a11y.announce('Group removed');
+ }
+
+ // Remove from maps
+ this.groupElements.delete(groupId);
+
+ // Clean up sortable
+ const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+ const sortable = this.sortableInstances.get(sortableKey);
+ if (sortable?.destroy) {
+ sortable.destroy();
+ }
+ this.sortableInstances.delete(sortableKey);
+
+ this.schedulePersistance(groupEl.fieldId);
+ }
+
+ addToGroup(uploadId, target = null, persist = true) {
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!upload || !uploadEl) return;
+
+ const fieldData = this.getFieldData(upload.fieldId);
+ const fieldEl = this.fieldElements.get(upload.fieldId);
+ if (!fieldData || !fieldEl) return;
+
+ // Already in correct location
+ if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
+ return;
+ }
+
+ // Remove from previous group
+ if (upload.groupId) {
+ const group = fieldData.groups?.find(g => g.id === upload.groupId);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ this.deleteGroup(upload.groupId);
+ }
+ }
+ }
+
+ // Clear selection checkbox
+ const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+ if (checkbox) checkbox.checked = false;
+
+ let featured = uploadEl.element.querySelector('[name="featured"]');
+ if (featured) featured.hidden = !target;
+
+ // Moving to preview or to group
+ if (!target || target.classList.contains('preview')) {
+ target = fieldEl.ui.preview;
+ upload.groupId = null;
+ } else {
+ // Moving to group
+ const groupId = target.dataset.groupId;
+ if (featured) featured.name = groupId + '_' + featured.name;
+
+ const group = fieldData.groups?.find(g => g.id === groupId);
+ if (group) {
+ if (!group.uploads) group.uploads = [];
+ group.uploads.push(uploadId);
+ upload.groupId = groupId;
+ }
+ }
+
+ // Update location
+ uploadEl.location = target;
+ target.append(uploadEl.element);
+
+ // Update stores
+ this.uploadStore.save(upload);
+ if (persist) {
+ this.saveFieldData(fieldData);
+ }
+
+ // Update sortable state
+ this.updateSortableState(target);
+ if (uploadEl.location && uploadEl.location !== target) {
+ this.updateSortableState(uploadEl.location);
+ }
+ }
+
+ removeFromGroup(uploadId) {
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+ if (!upload || !uploadEl) return;
+
+ const fieldData = this.getFieldData(upload.fieldId);
+ const fieldEl = this.fieldElements.get(upload.fieldId);
+ if (!fieldData || !fieldEl) return;
+
+ // Remove from current group
+ if (upload.groupId) {
+ const group = fieldData.groups?.find(g => g.id === upload.groupId);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ this.deleteGroup(upload.groupId, false);
+ }
+ }
+ upload.groupId = null;
+ }
+
+ // Move back to preview
+ if (fieldEl.ui?.preview) {
+ fieldEl.ui.preview.appendChild(uploadEl.element);
+ uploadEl.location = fieldEl.ui.preview;
+ }
+
+ // Hide featured radio
+ const featured = uploadEl.element.querySelector('[name="featured"]');
+ if (featured) {
+ featured.hidden = true;
+ featured.checked = false;
+ }
+
+ this.uploadStore.save(upload);
+ this.updateSortableState(fieldEl.ui.preview);
+ }
+
+ removeUpload(fieldId, uploadId) {
+ const fieldData = this.getFieldData(fieldId);
+ const upload = this.uploadStore.get(uploadId);
+ const uploadEl = this.uploadElements.get(uploadId);
+
+ if (!fieldData || !upload) return;
+
+ // Remove from field
+ fieldData.uploads?.delete(uploadId);
+
+ // Remove from group if grouped
+ if (upload.groupId) {
+ const group = fieldData.groups?.find(g => g.id === upload.groupId);
+ if (group) {
+ group.uploads = group.uploads.filter(id => id !== uploadId);
+ if (group.uploads.length === 0) {
+ this.deleteGroup(upload.groupId);
+ }
+ }
+ }
+
+ // Clean up element
+ uploadEl?.element?.remove();
+
+ // Clean up memory
+ this.clearUpload(uploadId);
+
+ // Update field state
+ this.saveFieldData(fieldData);
+ this.updateFieldState(fieldId);
+ this.maybeLockUploads(fieldId);
+
+ const handler = this.selectionHandlers.get(fieldId);
+ if (handler) {
+ handler.deselect(uploadId);
+ }
+
+ this.a11y.announce('Upload removed');
+ }
+
+ handleGroupMetaChange(input) {
+ const groupEl = this.getGroupFromElement(input);
+ if (!groupEl) return;
+
+ const fieldData = this.getFieldData(groupEl.fieldId);
+ const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
+ if (!group) return;
+
+ if (!group.changes) group.changes = {};
+
+ let name = input.name;
+ if (name.includes('group')) {
+ name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
+ }
+
+ group.changes[name] = input.value;
+ this.saveFieldData(fieldData);
+ this.schedulePersistance(groupEl.fieldId);
+ }
+
+ /*******************************************************************************
+ * ACTION HANDLERS
+ *******************************************************************************/
+
+ handleAction(button) {
+ const action = button.dataset.action;
+ const fieldId = this.getFieldIdFromElement(button);
+
+ switch(action) {
+ case 'add-to-group':
+ this.handleAddToGroup(button);
+ break;
+ case 'delete-group':
+ this.handleDeleteGroup(button);
+ break;
+ case 'delete-upload':
+ case 'remove-from-group':
+ this.handleRemoveItem(button);
+ break;
+ case 'upload':
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (fieldEl) {
+ fieldEl.element.closest('details').open = false;
+ document.body.classList.add('uploading');
+ this.submitUploads(fieldId);
+ }
+ break;
+ case 'restore':
+ this.handleRestoreUploads().then(() => {});
+ break;
+ case 'restore-all':
+ this.handleRestoreAll().then(() => {});
+ break;
+ case 'clear-cache':
+ if (!confirm('Save these uploads for later?')) {
+ this.cleanupStoredUploads();
+ }
+ this.cleanupRestore();
+ break;
+ }
+ }
+
+ handleAddToGroup(button) {
+ const fieldElement = button.closest(this.selectors.field.field);
+ const fieldId = fieldElement?.dataset.uploader;
+ if (!fieldId) return;
+
+ const selected = this.selected.get(fieldId);
+
+ if (!selected || selected.size === 0) {
+ this.createGroup(fieldId);
+ } else {
+ const group = this.createGroup(fieldId);
+ if (!group) return;
+
+ selected.forEach(uploadId => {
+ this.addToGroup(uploadId, group.grid);
+ });
+
+ const handler = this.selectionHandlers.get(fieldId);
+ handler?.clearSelection();
+
+ this.a11y.announce(`Created group with ${selected.size} items`);
+ }
+
+ this.schedulePersistance(fieldId);
+ }
+
+ handleDeleteGroup(button) {
+ const group = button.closest(this.selectors.groups.container);
+ if (!group) return;
+
+ const groupId = group.dataset.groupId;
+ const fieldId = this.getFieldIdFromElement(group);
+
+ if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+ return;
+ }
+
+ const items = group.querySelectorAll(this.selectors.items.item);
+ items.forEach(item => {
+ const uploadId = item.dataset.uploadId;
+ this.removeFromGroup(uploadId);
+ });
+
+ this.deleteGroup(groupId);
+ this.a11y.announce('Group deleted, items returned to upload area');
+ this.schedulePersistance(fieldId);
+ }
+
+ handleRemoveItem(button) {
+ const item = button.closest(this.selectors.items.item);
+ if (!item) return;
+
+ const uploadId = item.dataset.uploadId;
+ const fieldId = this.getFieldIdFromElement(item);
+
+ if (!confirm('Remove this item?')) return;
+
+ this.removeUpload(fieldId, uploadId);
+ this.a11y.announce('Item removed');
+ this.schedulePersistance(fieldId);
+ }
+
+ /*******************************************************************************
+ * SELECTION MANAGEMENT
+ *******************************************************************************/
+
+ addFieldSelectionHandler(fieldId) {
+ if (this.selectionHandlers.has(fieldId)) {
+ return this.selectionHandlers.get(fieldId);
+ }
+
+ const fieldEl = this.fieldElements.get(fieldId);
+ if (!fieldEl?.element) return;
+
+ const handler = new window.jvbHandleSelection({
+ container: fieldEl.element,
+ ui: {
+ selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
+ bulkControls: fieldEl.element.querySelector('.selection-actions'),
+ count: fieldEl.element.querySelector('.selection-count')
+ },
+ itemSelector: '[data-upload-id]',
+ checkboxSelector: '[name*="select-item"]'
+ });
+
+ handler.subscribe((event, data) => {
+ switch(event) {
+ case 'item-selected':
+ // Sync with Sortable
+ this.syncSortableSelection(fieldId, data.selectedItems);
+ this.selected.set(fieldId, data.selectedItems);
+ break;
+ case 'item-deselected':
+ this.syncSortableSelection(fieldId, data.selectedItems);
+ this.selected.set(fieldId, data.selectedItems);
+ break;
+ case 'range-selected':
+ this.syncSortableSelection(fieldId, data.selectedItems);
+ this.selected.set(fieldId, data.selectedItems);
+ break;
+ case 'select-all':
+ this.handleSelectAll(data.container, data.selected);
+ break;
+ }
+ });
+
+ this.selectionHandlers.set(fieldId, handler);
+ return handler;
+ }
+
+ addGroupSelectionHandler(fieldId, groupId) {
+ const handlerKey = `${fieldId}_${groupId}`;
+ if (this.selectionHandlers.has(handlerKey)) {
+ return this.selectionHandlers.get(handlerKey);
+ }
+
+ const groupEl = this.groupElements.get(groupId);
+ if (!groupEl?.element) return;
+
+ const handler = new window.jvbHandleSelection({
+ container: groupEl.element,
+ ui: {
+ selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
+ bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
+ count: groupEl.element.querySelector(this.selectors.groups.count)
+ },
+ itemSelector: '[data-upload-id]',
+ checkboxSelector: '[name*="select-item"]'
+ });
+
+ handler.subscribe((event, data) => {
+ switch(event) {
+ case 'item-selected':
+ case 'item-deselected':
+ case 'range-selected':
+ this.selected.set(fieldId, data.selectedItems);
+ break;
+ case 'select-all':
+ this.handleSelectAll(data.container, data.selected);
+ break;
+ }
+ });
+
+ this.selectionHandlers.set(handlerKey, handler);
+ return handler;
+ }
+
+ handleSelectAll(container, selected) {
+ // Can add custom logic here if needed
+ }
+
+ /*******************************************************************************
+ * HELPER METHODS
+ *******************************************************************************/
+
+ /**
+ * Get field data from store and normalize it
+ * Always use this instead of directly accessing fieldStore.get()
+ */
+ getFieldData(fieldId) {
+ const fieldData = this.fieldStore.get(fieldId);
+ if (!fieldData) return null;
+
+ // Only convert uploads back to Set (DataStore returns Arrays)
+ if (Array.isArray(fieldData.uploads)) {
+ fieldData.uploads = new Set(fieldData.uploads);
+ } else if (!fieldData.uploads) {
+ fieldData.uploads = new Set();
+ }
+
+ // Ensure groups is an array
+ if (!Array.isArray(fieldData.groups)) {
+ fieldData.groups = [];
+ }
+
+ return fieldData;
+ }
+
+ /**
+ * Save field data to store, converting Sets to Arrays
+ */
+ async saveFieldData(fieldData) {
+ await this.fieldStore.save({
+ ...fieldData,
+ timestamp: Date.now()
+ });
+ }
+
+ determineFieldId(fieldElement) {
+ const content = fieldElement.dataset.content+'_' ||
+ fieldElement.closest('dialog')?.dataset.content+'_' ||
+ fieldElement.closest('form')?.dataset.save+'_' || '';
+ const itemID = fieldElement.dataset.itemId+'_' ||
+ fieldElement.closest('dialog')?.dataset.itemId+'_' || '';
+ const field = fieldElement.dataset.field || '';
+
+ return `${content}${itemID}${field}`;
+ }
+
+ getFromElement(element, type) {
+ const map = {
+ 'field': {
+ selector: this.selectors.field.field,
+ key: 'uploader',
+ getRuntimeData: (id) => this.fieldElements.get(id),
+ getStoreData: (id) => this.getFieldData(id)
+ },
+ 'upload': {
+ selector: this.selectors.items.item,
+ key: 'uploadId',
+ getRuntimeData: (id) => this.uploadElements.get(id),
+ getStoreData: (id) => this.uploadStore.get(id)
+ },
+ 'group': {
+ selector: this.selectors.groups.container,
+ key: 'groupId',
+ getRuntimeData: (id) => this.groupElements.get(id),
+ getStoreData: (id) => {
+ // Groups are stored in field.groups array
+ const groupEl = this.groupElements.get(id);
+ if (!groupEl) return null;
+ const fieldData = this.getFieldData(groupEl.fieldId);
+ return fieldData?.groups?.find(g => g.id === id);
+ }
+ }
+ };
+
+ const config = map[type];
+ if (!config) return null;
+
+ const el = element.closest(config.selector);
+ if (!el) return null;
+
+ const id = el.dataset[config.key];
+
+ // Return combined runtime + store data for convenience
+ const runtime = config.getRuntimeData(id);
+ const store = config.getStoreData(id);
+
+ return { ...runtime, ...store };
+ }
+
+ getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+ getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+ getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+ getFieldIdFromElement(el) {
+ const field = this.getFromElement(el, 'field');
+ return field?.id ?? null;
+ }
+ getUploadIdFromElement(el) {
+ const upload = this.getFromElement(el, 'upload');
+ return upload?.id ?? null;
+ }
+ getGroupIdFromElement(el) {
+ const group = this.getFromElement(el, 'group');
+ return group?.id ?? null;
+ }
+
+ getSubtypeFromMime(mimeType) {
+ if (mimeType.startsWith('image/')) return 'image';
+ if (mimeType.startsWith('video/')) return 'video';
+ return 'document';
+ }
+
+ getStatusText(status) {
+ return this.statusMapping[status] || status;
+ }
+
+ getStatusIcon(status) {
+ return window.getIcon(this.queue.icons[status]);
+ }
+
+ getStatusProgress(status) {
+ const progress = {
+ 'local_processing': 28,
+ 'queued': 50,
+ 'uploading': 66,
+ 'pending': 75,
+ 'processing': 89,
+ 'completed': 100
+ };
+ return progress[status] || 0;
+ }
+
+
+ createUploadElement(upload, draggable = false) {
+ let image = window.getTemplate('uploadItem');
+ if (!image) return;
+
+ image.dataset.uploadId = upload.id;
+ image.dataset.subtype = upload.subtype || 'image';
+
+ let [featured, img, video, preview, details] = [
+ image.querySelector('[name="featured"]'),
+ image.querySelector('img'),
+ image.querySelector('video'),
+ image.querySelector('label > span'),
+ image.querySelector('details')
+ ];
+
+ if (featured) featured.value = upload.id;
+
+ switch (upload.subtype) {
+ case 'image':
+ if (img) {
+ img.src = upload.preview;
+ img.alt = upload.meta?.originalName || '';
+ }
+ video?.remove();
+ preview?.remove();
+ break;
+ case 'video':
+ if (video) video.src = upload.preview;
+ img?.remove();
+ preview?.remove();
+ break;
+ case 'document':
+ const fileName = upload.meta?.originalName || '';
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
+ const iconMap = {
+ 'pdf': 'file-pdf', 'csv': 'file-csv',
+ 'doc': 'file-doc', 'docx': 'file-doc',
+ 'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+ };
+ const icon = window.getIcon(iconMap[extension] || 'file');
+ if (preview) {
+ preview.innerText = fileName;
+ preview.prepend(icon);
+ }
+ img?.remove();
+ video?.remove();
+ break;
+ }
+
+ if (details) {
+ let template = window.getTemplate('uploadMeta');
+ if (template) details.append(template);
+ }
+
+ image.draggable = draggable;
+
+ // Update input IDs
+ image.querySelectorAll('input').forEach(input => {
+ let id = input.id;
+ if (id) {
+ let newId = id + upload.id;
+ let label = input.parentNode.querySelector(`label[for="${id}"]`);
+ input.id = newId;
+ if (label) label.htmlFor = newId;
+ }
+ });
+
+ return image;
+ }
+
+ /*******************************************************************************
+ * PERSISTENCE
+ *******************************************************************************/
+
+ schedulePersistance(fieldId) {
+ const key = `persist_${fieldId}`;
+ window.debouncer.schedule(
+ key,
+ () => this.persistFieldState(fieldId),
+ 250
+ );
+ }
+
+ async persistFieldState(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData) return;
+
+ // Save with updated timestamp
+ await this.saveFieldData(fieldData);
+ }
+
+ // In UploadManager, add blob conversion helpers
+ async saveBlobData(uploadId, file) {
+ const arrayBuffer = await file.arrayBuffer();
+
+ const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
+
+ // Store blob data as ArrayBuffer with metadata
+ uploadData.blobData = {
+ buffer: arrayBuffer,
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ lastModified: file.lastModified || Date.now()
+ };
+
+ await this.uploadStore.save(uploadData);
+ }
+
+ async getBlobData(uploadId) {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload?.blobData) return null;
+
+ // Reconstruct File from ArrayBuffer
+ const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
+ return new File([blob], upload.blobData.name, {
+ type: upload.blobData.type,
+ lastModified: upload.blobData.lastModified
+ });
+ }
+
+ /*******************************************************************************
+ HELPER to GET UPLOADED FILES
+ *******************************************************************************/
+ /**
+ * Get all files for a form (searches all upload fields within form)
+ */
+ async getFilesForForm(formElement) {
+ const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+ const allFiles = [];
+
+ for (const field of uploadFields) {
+ const fieldId = this.determineFieldId(field);
+ const files = await this.getFilesForField(fieldId);
+ allFiles.push(...files);
+ }
+
+ return allFiles;
+ }
+
+ /**
+ * Get all files for a specific field
+ */
+ async getFilesForField(fieldId) {
+ const fieldData = this.getFieldData(fieldId);
+ if (!fieldData?.uploads) return [];
+
+ const files = [];
+ const uploadsArray = fieldData.uploads instanceof Set
+ ? Array.from(fieldData.uploads)
+ : fieldData.uploads;
+
+ for (const uploadId of uploadsArray) {
+ const upload = this.uploadStore.get(uploadId);
+ if (!upload) continue;
+
+ // Get the actual File object from blob data
+ const file = await this.getBlobData(uploadId);
+ if (file) {
+ files.push({
+ file: file,
+ uploadId: uploadId,
+ fieldName: fieldData.config.name,
+ meta: upload.meta || {}
+ });
+ }
+ }
+
+ return files;
+ }
+
+ /*******************************************************************************
+ * RECOVERY & RESTORATION
+ *******************************************************************************/
+
+ handleFieldStoreEvent(event, data) {
+ switch(event) {
+ case 'data-loaded':
+ this.fieldStoreReady = true;
+ this.checkIfBothStoresReady();
+ break;
+ }
+ }
+
+ handleUploadStoreEvent(event, data) {
+ switch(event) {
+ case 'data-loaded':
+ this.uploadStoreReady = true;
+ this.checkIfBothStoresReady();
+ break;
+ case 'item-saved':
+ this.showSaveIndicator(data.key);
+ break;
+ }
+ }
+
+ checkIfBothStoresReady() {
+ if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
+ this.hasCheckedForUploads = true;
+ this.checkForStoredUploads();
+ }
+ }
+
+ async checkForStoredUploads() {
+ const allFieldStates = this.fieldStore.getAll();
+
+ const pendingFields = allFieldStates.filter(field => {
+ if (!field.uploads) return false;
+
+ // Handle both Set and Array (from IndexedDB)
+ const uploadsArray = field.uploads instanceof Set
+ ? Array.from(field.uploads)
+ : Array.isArray(field.uploads)
+ ? field.uploads
+ : [];
+
+ return uploadsArray.some(uploadId => {
+ const upload = this.uploadStore.get(uploadId);
+ return upload && !upload.operationId &&
+ ['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
+ });
+ });
+ if (pendingFields.length === 0) return;
+
+ await this.showRecoveryNotification(pendingFields);
+ }
+
+ async showRecoveryNotification(pendingFields) {
+ const totalUploads = pendingFields.reduce((sum, field) => sum + field.uploads.length, 0);
+ const totalGroups = pendingFields.reduce((sum, field) =>
+ sum + (field.groups?.length || 0), 0);
+
+ let notification = window.getTemplate('restoreNotification');
+ if (!notification) {
+ console.error('Restore notification template not found');
+ return;
+ }
+
+ // Build appropriate message
+ let message;
+ if (totalGroups > 0) {
+ let group = totalGroups > 1 ? 'groups' : 'group';
+ let upload = totalUploads > 1 ? 'uploads' : 'upload';
+ message = `${totalGroups} ${group} with ${totalUploads} ${upload} can be restored.`;
+ } else {
+ message = `${totalUploads} upload(s) from ${pendingFields.length} field(s) can be recovered.`;
+ }
+
+ const detailsEl = notification.querySelector('.restore-details');
+ if (detailsEl) {
+ detailsEl.textContent = message;
+ }
+
+ // Build the restoration preview
+ for (const field of pendingFields) {
+ let fieldTemplate = window.getTemplate('restoreField');
+ if (!fieldTemplate) continue;
+
+ // Set field name/title
+ const titleEl = fieldTemplate.querySelector('h3');
+ if (titleEl) {
+ titleEl.textContent = field.config.name || 'Unnamed Field';
+ }
+
+ const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
+
+ // Process each upload
+ for (let uploadId of field.uploads) {
+ const upload = this.uploadStore.get(uploadId);
+ let uploadItem = window.getTemplate('uploadItem');
+ if (!uploadItem) continue;
+ //
+ // const imgEl = uploadItem.querySelector('img');
+ // const placeholderEl = uploadItem.querySelector('.image-placeholder');
+ //
+ const file = await this.getBlobData(upload.id);
+ if (file) {
+
+ try {
+ // Create new blob URL from stored data
+ const previewUrl = this.createPreviewUrl(file);
+
+ let [
+ featured,
+ img,
+ video,
+ preview,
+ details
+ ] = [
+ uploadItem.querySelector('[name="featured"]'),
+ uploadItem.querySelector('img'),
+ uploadItem.querySelector('video'),
+ uploadItem.querySelector('label > span'),
+ uploadItem.querySelector('details')
+ ];
+
+ uploadItem.dataset.uploadId = upload.id;
+
+
+ uploadItem.dataset.fieldId = field.id;
+
+ let subtype = this.getSubtypeFromMime(file.type);
+ uploadItem.dataset.subtype = subtype;
+ switch (subtype) {
+ case 'image':
+ [
+ img.src,
+ img.alt
+ ] = [
+ previewUrl,
+ file.name ?? upload.meta?.originalName ?? ''
+ ];
+ video.remove();
+ preview.remove();
+ break;
+ case 'video':
+ video.src = previewUrl;
+ img.remove();
+ preview.remove();
+ break;
+ case 'document':
+ let extension = '';
+ let icon;
+ switch (extension) {
+ case 'pdf':
+ icon = window.getIcon('file-pdf');
+ break;
+ case 'csv':
+ icon = window.getIcon('file-csv');
+ break;
+ case 'doc':
+ icon = window.getIcon('file-doc');
+ break;
+ case 'txt':
+ icon = window.getIcon('file-txt');
+ break;
+ case 'xls':
+ icon = window.getIcon('file-xls');
+ break;
+ default:
+ icon = window.getIcon('file');
+ break;
+ }
+
+ preview.innerText = upload.originalFile.name;
+ preview.prepend(icon);
+ img.remove();
+ video.remove();
+ break;
+ }
+
+ // Store URL for cleanup later
+ uploadItem.dataset.previewUrl = previewUrl;
+ } catch (error) {
+ console.warn('Failed to create preview for upload:', upload.id, error);
+ }
+ }
+
+ // Set upload metadata
+ const nameEl = uploadItem.querySelector('summary span');
+ if (nameEl) {
+ nameEl.textContent = upload.meta?.originalName || 'Unknown file';
+ }
+
+ const metaEl = uploadItem.querySelector('details');
+ if (metaEl && upload.meta) {
+ metaEl.textContent = `${this.formatBytes(upload.meta.size)} • ${upload.meta.type}`;
+ }
+
+ // Update input IDs safely
+ uploadItem.querySelectorAll('input').forEach(input => {
+ let id = input.id;
+ if (id) {
+ let newId = id + upload.id;
+ let label = input.parentNode.querySelector(`label[for="${id}"]`);
+ input.id = newId;
+ if (label) {
+ label.htmlFor = newId;
+ }
+ }
+ });
+
+ if (itemGrid) {
+ itemGrid.appendChild(uploadItem);
+ }
+ }
+
+ notification.querySelector('.wrap').appendChild(itemGrid);
+ }
+
+ document.querySelector('.field.upload').appendChild(notification);
+ notification = document.querySelector('dialog.restore-uploads');
+ this.restoreModal = new window.jvbModal(notification);
+ this.restoreSelection = new window.jvbHandleSelection({
+ container: notification,
+ ui: {
+ selectAll: notification.querySelector('#select-all-restore'),
+ count: notification.querySelector('.selection-count'),
+ },
+ });
+
+ this.restoreModal.handleOpen();
+
+ }
+
+ async handleRestoreUploads() {
+ let notification = document.querySelector('dialog.restore-uploads');
+ if (!notification) {
+ return;
+ }
+
+ const selectedUploads = this.getSelectedRestorationUploads(notification);
+ if (selectedUploads.length === 0) {
+ return;
+ }
+ await this.restoreSelectedUploads(selectedUploads);
+
+ this.cleanupRestore();
+ }
+
+ async handleRestoreAll() {
+ let notification = document.querySelector('dialog.restore-uploads');
+ if (!notification) {
+ return;
+ }
+ // Gets ALL uploads from notification without checking selection
+ const allUploads = [];
+ notification.querySelectorAll('.item.upload').forEach(item => {
+ let uploadId = item.dataset.uploadId;
+ let fieldId = item.dataset.fieldId;
+ allUploads.push({ uploadId, fieldId });
+ });
+
+ await this.restoreSelectedUploads(allUploads);
+ this.cleanupRestore();
+ }
+
+ showSaveIndicator(key) {
+ // Optional: show user that state is being saved
+ }
+
+ cleanupRestore() {
+ this.restoreModal.handleClose();
+ this.restoreSelection.destroy();
+ this.restoreSelection = null;
+ this.restoreModal.destroy();
+ this.restoreModal.modal.remove();
+ this.restoreModal = null;
+ }
+
+ async cleanupStoredUploads() {
+ await this.fieldStore.clear();
+ await this.uploadStore.clear();
+ }
+
+ /*******************************************************************************
+ * EVENT SYSTEM
+ *******************************************************************************/
+
+ subscribe(callback) {
+ this.subscribers.add(callback);
+ return () => this.subscribers.delete(callback);
+ }
+
+ notify(event, data = {}) {
+ this.subscribers.forEach(cb => {
+ try {
+ cb(event, data);
+ } catch (error) {
+ console.error('Subscriber error:', error);
+ }
+ });
+ }
+
+ /*******************************************************************************
+ * DESTROY & CLEANUP
+ *******************************************************************************/
+
+ destroy() {
+ document.removeEventListener('click', this.clickHandler);
+ document.removeEventListener('change', this.changeHandler);
+ document.removeEventListener('dragenter', this.dragEnterHandler);
+ document.removeEventListener('dragleave', this.dragLeaveHandler);
+ document.removeEventListener('dragover', this.dragOverHandler);
+ document.removeEventListener('drop', this.dropHandler);
+
+ if (this.dragController) {
+ this.dragController.destroy();
+ }
+
+ this.selectionHandlers.forEach(handler => handler.destroy());
+ this.selectionHandlers.clear();
+
+ this.cleanupAllPreviewUrls();
+
+ this.sortableInstances.forEach(instance => {
+ if (instance?.destroy) instance.destroy();
+ });
+ this.sortableInstances.clear();
+
+ this.uploadElements.clear();
+ this.fieldElements.clear();
+ this.groupElements.clear();
+ this.selected.clear();
+ this.subscribers.clear();
+ }
+}
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', async function () {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ window.jvbUploads = new UploadManager();
+ }
+ });
+});
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index 3b9e355..4a7b85b 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -274,6 +274,26 @@
return div.innerHTML;
}
+window.generateID = function(prefix = 'jvb') {
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
+}
+
+window.showProgress = function(elements, current, total, message = '', icon = '') {
+ const show = current < total;
+ if (elements.progress && show) {
+ window.fade(elements.progress, true);
+ }
+ const percent = total > 0 ? (current / total) * 100 : 0;
+ if (elements.fill) elements.fill.style.width = `${percent}%`;
+ if (elements.details) elements.details.textContent = message;
+ if (elements.count) elements.count.textContent = `${current}/${total}`;
+ if (elements.icon) elements.icon.className = (icon === '') ? 'icon' : 'icon icon-'+icon;
+
+ if (elements.progress && current === total) {
+ window.fade(elements.progress, false);
+ }
+}
+
/**
* Format a date string for display
* @param {string} dateString - ISO date string
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index 77c15e9..cc2648d 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -72,7 +72,7 @@
this.toggleNav(!nav.classList.contains('open'), nav.id);
}
- let submenuToggle = e.target.closest('[data-action="toggle-submenu"]')
+ let submenuToggle = e.target.closest('[data-action="toggle-submenu"], .has-submenu .a')
if (submenuToggle) {
let li = submenuToggle.closest('li');
this.toggleSubmenu(!li.classList.contains('open'), li);
diff --git a/assets/js/min/crud.min.js b/assets/js/min/crud.min.js
index 3a56d7e..8723a48 100644
--- a/assets/js/min/crud.min.js
+++ b/assets/js/min/crud.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"content"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let a=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&a.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let o=t.config.element.querySelectorAll(".selected input:checked");o.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&a.push(e.value)})),s=`Updating ${o.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(a.length>0){let e=0;a.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const a=this.store.get(i);if(!a)return;a.fields[l]=this.getInputValue(s),this.store.save(a);let o={};o[i]=a.fields,this.savePosts(o,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),a=s.closest("tr.timeline-point"),o=this.store.get(l);if(!o)return;const n=this.getInputValue(s);if(a){const e=a.dataset.imageId;o.fields.timeline||(o.fields.timeline={}),o.fields.timeline[e]||(o.fields.timeline[e]={}),o.fields.timeline[e][i]=n}else o.fields[i]=n;this.store.save(o);let r={};r[l]=o.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),a=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),a&&s.thumbnail&&(a.src=s.thumbnail,a.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e){if(this.queue=window.jvbQueue,this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.a11y=window.jvbA11y,!this.content)return;this.isTimeline=!1,this.currentItemID=null,this.initElements(),this.updateBulkOptions();const t=window.jvbStore.register(this.content,{storeName:this.content,keyPath:"id",endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},indexes:[{name:"id",keyPath:"id"},{name:"status",keyPath:"status"},{name:"date",keyPath:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:{content:this.content,user:window.auth.getUser(),page:1,status:"all",orderby:"modified",order:"desc"},TTL:18e5,showLoading:!0});this.store=t[this.content],this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.tableForm=null,this.tableChanges=new Map,this.formController=this.isTimeline?new window.jvbForm({collectFormData:()=>this.collectTimelineData.bind(this)}):new window.jvbForm,this.viewController.subscribe(((e,t)=>{if("table-view"!==e||this.tableForm){if("not-table-view"===e)this.tableForm;else if("order-changed"===e){let e=this.store.get(t);if(!e)return;let s={};s[t]=e,this.savePosts(s,"Updating progression order")}}else this.tableForm||(this.tableForm=this.formController.registerForm(t,{autosave:!1,formStatus:!1,isTable:!0}))})),this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&["content","uploads/groups"].includes(t.endpoint)&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initialized=!1,this.init()}handleFormChange(e,t){let s=t.fullData.post_title,i=Object.hasOwn(t,"changes")?t.changes:t.fullData,l={};if(this.isTimeline)return l[this.currentItemID]=i,void this.savePosts(l,s);let o=[];switch(!0){case t.config.element===this.ui.forms.edit:l[this.currentItemID]=i,s=`Saving ${s} Changes`,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(this.currentItemID);break;case t.config.element===this.ui.forms.bulkEdit:let a=t.config.element.querySelectorAll(".selected input:checked");a.forEach((e=>{l[e.value]=i,i.post_status&&this.shouldRemoveItem(i.post_status)&&o.push(e.value)})),s=`Updating ${a.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(l[t.config.data["form-id"]]=i,s=`Saving ${s} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}0!==Object.keys(l).length&&this.savePosts(l,s)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(0===Object.keys(e).length)return;for(let t in e)e[t].content||(e[t].content=this.content);let s={endpoint:"content",headers:{action_nonce:window.auth.getNonce("dash")},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}async handleQueueSuccess(e,t){this.store.clearCache(),this.store.fetch()}handleQueueFailure(e,t){console.error("Operation failed permanently:",t),this.a11y?.announce(`Operation failed: ${t.error_message||"Unknown error"}`)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.ui.uploader&&(window.jvbUploads.scanFields(document.querySelector(this.elements.uploader)),window.jvbUploads.subscribe(((e,t)=>{"sent-to-queue"===e&&(console.log(t),t===this.ui.uploader.querySelector("[data-uploader]")?.dataset.uploader&&window.debouncer.schedule("crud-complete",(()=>{this.store.clearHttpHeaders()})))}))),this.isTimeline=!!document.querySelector("[data-timeline]")}init(){this.ui.uploader&&(this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")}))),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.currentItemID=null,this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId)}));this.setupEventDelegation(),this.setupFilters(),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemId="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.closest("[data-id]"))this.isTimeline?this.handleTimelineTableChange(e):this.handleTableChange(e);else{if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}window.targetCheck(e,"select[data-filter]")&&this.handleFilterChange(e)}}handleTableChange(e){const t=e.target.closest("tr[data-id]");if(!t)return;const s=e.target,i=parseInt(t.dataset.id),l=s.closest(["data-field"])?.dataset.field;if(!l)return;const o=this.store.get(i);if(!o)return;o.fields[l]=this.getInputValue(s),this.store.save(o);let a={};a[i]=o.fields,this.savePosts(a,`Saving changes to ${this.content}`)}handleTimelineTableChange(e){const t=e.target.closest("tbody[data-id]");if(!t)return;const s=e.target,i=s.closest("[data-field]")?.dataset.field;if(!i)return;const l=parseInt(t.dataset.id),o=s.closest("tr.timeline-point"),a=this.store.get(l);if(!a)return;const n=this.getInputValue(s);if(o){const e=o.dataset.imageId;a.fields.timeline||(a.fields.timeline={}),a.fields.timeline[e]||(a.fields.timeline[e]={}),a.fields.timeline[e][i]=n}else a.fields[i]=n;this.store.save(a);let r={};r[l]=a.fields,this.savePosts(r,"Updating progress post")}getInputValue(e){return"checkbox"===e.type?e.checked?e.value||"1":"":"radio"===e.type?e.checked?e.value:null:e.value}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);i.forEach((i=>{s[i]={content:this.content},s[i][t]=e}));let l=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,l)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if("all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),0!==Object.keys(s).length&&this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target;if("taxonomies"===t.dataset.filter){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,t.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions?.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(this.ui.bulkSelectActions&&!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions&&(this.ui.bulkSelectActions.value="")}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){let s=this.store.get(t);const i=window.getTemplate("bulkItem");if(!i)return;const l=i.querySelector("input[type=checkbox]"),o=i.querySelector("img");l&&(l.id=`bulk_${s.id}`,l.value=s.id,l.checked=!0),o&&s.thumbnail&&(o.src=s.thumbnail,o.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit)}populateEditForm(e){this.currentItemID=e;let t=this.store.get(parseInt(e));if(t){this.ui.modals.edit.dataset.itemId=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit)}}setupFilters(){const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{if("auth-loaded"===t){let t=document.querySelector("[data-content]");t&&!Object.hasOwn(t.dataset,"ignore")&&(window.crudManager=new e({content:t.dataset.content}))}}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 3fcaee1..ea47195 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let i=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,r={config:{dbName:i.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};r.config.headers={"X-WP-Nonce":window.auth.getNonce(),...r.config.headers},i.stores[t.storeName]=s,this.stores.set(s,r),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const r={};for(const[e,t]of Object.entries(i.stores))r[e]=this.getStoreAPI(t);return r}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,i]of e.entries())i instanceof File||i instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(i)):t.entries[s]=i);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,i]of Object.entries(e.entries))Array.isArray(i)?i.forEach((e=>t.append(s,e))):t.append(s,i);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:i}=t,r=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,i,(e=>{r.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}r.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((i,r)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>i(e.target.result),a.onerror=e=>r(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const i=this.stores.get(e);return i?.db&&i.db.objectStoreNames.contains(t)?new Promise((e=>{const r=i.db.transaction([t],"readonly").objectStore(t).getAll();r.onsuccess=t=>{const i=t.target.result||[];i.forEach(s),e(i)},r.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,i){const r=this.stores.get(e);return r?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=r.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>a(o.error);try{h=i(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),i=t.cache.get(s);if(i&&this.isCacheValid(i,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:i.items||[]}),i;t.config.showLoading&&this.setLoading(!0);const r=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&i&&(i.etag&&(a["If-None-Match"]=i.etag),i.lastModified&&(a["If-Modified-Since"]=i.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(r,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return i?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:i.items||[]}),i):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const i=t.config.apiBase+t.config.endpoint;return s.toString()?`${i}?${s}`:i}async processFetchedData(e,t,s,i){const r=this.stores.get(e),a=t.items||[],o=[];r.db&&a.length>0&&await this.withTransaction(e,r.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const i=this._saveItem(e,s);o.push(i),t.put(i.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters},etag:i.headers.get("ETag"),lastModified:i.headers.get("Last-Modified")};r.cache.set(s,n),r.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_saveItem(e,t){const s=this.stores.get(e),i=this.processForStorage(t,s.config.validateData);if(!i.valid)throw new Error(`Non-serializable data: ${i.error}`);const r=i.data,a=this.getItemKey(r,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:r,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),i=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(i.processed)})),this.notify(e,"item-saved",{item:i.item,key:i.key,previousItem:i.previousItem}),i.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const i=typeof e;if(["string","number","boolean"].includes(i))return{valid:!0,data:e};if("function"===i)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:null};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:null};if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0,data:e};if(e instanceof Set){const i=Array.from(e);return this.processForStorage(i,t,s)}if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const i=[];for(let r=0;r<e.length;r++){const a=this.processForStorage(e[r],t,`${s}[${r}]`);if(!a.valid)return a;null!==a.data&&i.push(a.data)}return{valid:!0,data:i}}if("object"===i){const i={};for(const[r,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${r}`);if(!e.valid)return e;null!==e.data&&(i[r]=e.data)}return{valid:!0,data:i}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:null}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),i=t.cache.get(s);return i&&i.items?i.items.reduce(((e,s)=>{const i=t.data.get(s);return i&&e.push(i),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const i=this.stores.get(e),r={...i.filters};s?i.filters={...i.config.filters}:Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete i.filters[e]:i.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:r,filters:i.filters,updates:t}),i.config.endpoint&&await this.fetch(e)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const i=this.subscribers.get(e);i&&i.forEach((i=>{try{i(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let i=e;for(const e of s)i=i?.[e];return i}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let d;o.oncomplete=()=>e(d),o.onerror=()=>a(o.error);try{d=r(c,o)}catch(e){a(e)}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t})),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=t.items||[],o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified")};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}},o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:(console.debug(`[DataStore] Stripped function at ${s}`),{valid:!0,data:void 0});if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:(console.debug(`[DataStore] Stripped DOM element at ${s}`),{valid:!0,data:void 0});if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:(console.debug(`[DataStore] Converted FormData at ${s}`),{valid:!0,data:this.formDataToObject(e)});if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0!==e.data&&(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:(console.debug(`[DataStore] Stripped unknown type at ${s}`),{valid:!0,data:void 0})}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}get(e,t){return this.stores.get(e).data.get(t)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);return r&&r.items?r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s?r.filters={...r.config.filters}:Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t}),r.config.endpoint&&await this.fetch(e)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e])))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/handleSelection.min.js b/assets/js/min/handleSelection.min.js
index eb95d96..e97ee16 100644
--- a/assets/js/min/handleSelection.min.js
+++ b/assets/js/min/handleSelection.min.js
@@ -1 +1 @@
-window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.ui=e.ui||{},this.itemSelector=e.itemSelector||".item",this.checkboxSelector=e.checkboxSelector||'[name*="select-item"]',this.selectedItems=new Set,this.lastSelected=null,this.subscribers=new Set,this.init()}init(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("keydown",this.keyHandler)}handleKeys(e){(e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.ui.selectAll&&(this.ui.selectAll.checked=!0,this.selectAll(!0),window.jvbA11y&&window.jvbA11y.announce("All items selected"))),"Escape"===e.key&&this.selectedItems.size>0&&(this.selectAll(!1),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")),("Delete"===e.key||"Backspace"===e.key)&&!e.target.matches("input, textarea")&&this.selectedItems>0&&(e.preventDefault(),confirm(`Remove ${this.selectedItems.size} selected item${1!==this.selectedItems.size?"s":""}?`)&&this.deselect(this.selectedItems))}handleClick(e){const t=e.target.closest(`${this.checkboxSelector}, label[for]`);if(!t)return;const s="LABEL"===t.tagName?document.getElementById(t.getAttribute("for")):t;if(s)if(e.shiftKey&&this.lastSelected)e.preventDefault(),this.handleRangeSelection(s);else{const e=s.closest(this.itemSelector);e&&(this.lastSelected=e)}}handleChange(e){if(this.ui.selectAll&&e.target===this.ui.selectAll)this.selectAll(e.target.checked);else{const t=e.target.closest(this.checkboxSelector);if(!t)return;this.toggleSelection(this.getItemId(t))}}toggleSelection(e){if(!e)return;let t=!0;this.selectedItems.has(e)?(t=!1,this.selectedItems.delete(e)):this.selectedItems.add(e),t?this.notify("item-selected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}):this.notify("item-deselected",{selectedItem:e,selectedItems:this.selectedItems,container:this.container}),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(this.itemSelector);e||(this.selectedItems.clear(),this.ui.selectAll&&(this.ui.selectAll.checked=!1)),t.forEach((t=>{const s=this.getItemId(t),i=t.querySelector(this.checkboxSelector);i&&(i.checked=e),e&&s&&this.selectedItems.add(s)})),this.notify("select-all",{container:this.container,selected:e,items:t}),this.updateSelectionUI()}clearSelection(){this.selectAll(!1)}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(this.itemSelector));const t=e.closest(this.itemSelector);if(!t)return;const s=Array.from(this.container.querySelectorAll(this.itemSelector)),i=s.indexOf(this.lastSelected),c=s.indexOf(t);if(-1===i||-1===c)return;const l=Math.min(i,c),n=Math.max(i,c);let h=!e.checked;for(let e=l;e<=n;e++){const t=s[e],i=t.querySelector(this.checkboxSelector),c=this.getItemId(t);i&&c&&(i.checked=h,this.selectedItems.add(c))}this.lastSelected=t,this.updateSelectionUI(),this.notify("range-selected",{selectedItems:this.selectedItems,container:this.container});const r=n-l+1;window.jvbA11y&&window.jvbA11y.announce(`Selected ${r} items in range`)}updateSelectionUI(){const e=this.selectedItems.size,t=this.container.querySelectorAll(this.itemSelector).length;if(this.ui.bulkControls&&(this.ui.bulkControls.hidden=0===e),this.ui.count){const t=1===e?"item":"items";this.ui.count.textContent=0===e?"":`{ ${e} ${t} selected }`,this.ui.count.hidden=0===e}if(this.ui.selectAll){this.ui.selectAll.checked=t>0&&e===t,this.ui.selectAll.indeterminate=e>0&&e<t;const s=this.ui.selectAll.nextElementSibling||this.ui.selectAll.previousElementSibling;s&&"LABEL"===s.tagName&&(s.textContent=t>0&&e===t?"Clear Selection":"Select All")}}getItemId(e){const t=e.closest(this.itemSelector);return t?t.dataset.id||t.dataset.itemId||t.dataset.uploadId||t.id:null}isSelected(e){return this.selectedItems.has(e)}select(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.add(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!0)}})),this.updateSelectionUI(),this.notify("item-selected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}deselect(e){(Array.isArray(e)?e:[e]).forEach((e=>{this.selectedItems.delete(e);const t=this.container.querySelector(`${this.itemSelector}[data-id="${e}"]`);if(t){const e=t.querySelector(this.checkboxSelector);e&&(e.checked=!1)}})),this.updateSelectionUI(),this.notify("item-deselected",{selectedItem:id,selectedItems:this.selectedItems,container:this.container})}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.container&&(this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("keydown",this.keyHandler)),this.clearSelection(),this.subscribers.clear(),this.container=null,this.ui=null,this.lastSelected=null}};
\ No newline at end of file
+window.jvbHandleSelection=class{constructor(e){this.container=e.container,this.selectors={item:e.item||".item",count:e.count||".selection-count",bulkControls:e.bulkControls||".selection-actions",checkbox:e.checkbox||'[name*="select-item"]',selectAll:e.selectAll||"[data-select-all]",wrapper:e.wrapper||":has(.item-grid)"},this.ui=window.uiFromSelectors(this.selectors,this.container),this.selectedItems=new Set,this.lastSelected=null,this.lastSelectedWrapper=null,this.lastClicked=null,this.subscribers=new Set,this.initListeners()}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.keyHandler=this.handleKeys.bind(this),this.container.addEventListener("change",this.changeHandler),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("keydown",this.keyHandler)}handleChange(e){if(e.target.matches(this.selectors.selectAll))this.handleSelectAll(e.target);else if(e.target.matches(this.selectors.checkbox)){const t=e.target.closest(this.selectors.item);if(!t)return;const s=this.getItemWrapper(t),i=this.getItemId(t);this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&this.clearSelection(),e.target.checked?this.select(i,!1):this.deselect(i,!1),this.lastSelected=i,this.lastSelectedWrapper=s}}handleClick(e){const t=e.target.closest(this.selectors.item),s=t?this.getItemWrapper(t):null;if(s&&(this.lastClicked=s),t&&!e.target.matches(this.selectors.checkbox)&&this.lastSelectedWrapper&&s&&s!==this.lastSelectedWrapper&&!e.shiftKey&&(this.clearSelection(),this.lastSelectedWrapper=s),!e.shiftKey)return;if(!e.target.closest(this.selectors.checkbox)||!this.lastSelected||!this.lastSelectedWrapper)return;if(!t||!s)return;if(s!==this.lastSelectedWrapper)return;const i=Array.from(s.querySelectorAll(this.selectors.item)),l=this.getItemId(t),c=i.findIndex((e=>this.getItemId(e)===this.lastSelected)),r=i.findIndex((e=>this.getItemId(e)===l));if(-1===c||-1===r)return;const[h,n]=[Math.min(c,r),Math.max(c,r)];i.slice(h,n+1).forEach((e=>{this.select(this.getItemId(e))})),this.notify("range-selected",{selectedItems:new Set(this.selectedItems),wrapper:s})}getItemWrapper(e){if(!e)return null;const t=this.selectors.wrapper.split(",").map((e=>e.trim()));for(const s of t){const t=e.closest(s);if(t)return t}return null}handleKeys(e){if((e.ctrlKey||e.metaKey)&&"a"===e.key&&(e.preventDefault(),this.lastClicked)){let e=this.lastClicked.querySelector(this.selectors.selectAll);e&&(e.checked=!0)}"Escape"===e.key&&this.selectedItems.size>0&&(this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared"))}handleSelectAll(e){const t=this.getItemWrapper(e)||e.closest(this.selectors.wrapper);if(!t)return;this.lastSelectedWrapper&&t!==this.lastSelectedWrapper&&this.clearSelection();const s=t.querySelectorAll(this.selectors.item),i=Array.from(s).map((e=>this.getItemId(e)));e.checked?(i.forEach((e=>this.select(e,!0,!1))),this.lastSelectedWrapper=t):(i.forEach((e=>this.deselect(e,!0,!1))),0===this.selectedItems.size&&(this.lastSelectedWrapper=null));let l=e.nextElementSibling||e.previousElementSibling;l&&"LABEL"===l.tagName&&(l.textContent=e.checked&&s.length>0?"Clear Selection":"Select All"),this.updateSelectionUI(),this.notify("select-all",{wrapper:t,checked:e.checked,ids:i,selectedItems:new Set(this.selectedItems)})}getItemId(e){return e.dataset.uploadId}select(e,t=!0,s=!0){this.selectedItems.has(e)||(this.selectedItems.add(e),t&&this.setCheckboxState(e,!0),s&&this.updateSelectionUI(),this.notify("item-selected",{id:e,selectedItems:new Set(this.selectedItems)}))}deselect(e,t=!0,s=!0){this.selectedItems.has(e)&&(this.selectedItems.delete(e),t&&this.setCheckboxState(e,!1),s&&this.updateSelectionUI(),this.notify("item-deselected",{id:e,selectedItems:new Set(this.selectedItems)}))}toggle(e){this.selectedItems.has(e)?this.deselect(e):this.select(e),this.updateSelectionUI()}clearSelection(){this.selectedItems.forEach((e=>this.setCheckboxState(e,!1))),this.selectedItems.clear(),this.lastSelected=null,this.lastSelectedWrapper=null,this.container.querySelectorAll(this.selectors.selectAll).forEach((e=>{e.checked=!1;const t=e.nextElementSibling||e.previousElementSibling;"LABEL"===t?.tagName&&(t.textContent="Select All")})),this.updateSelectionUI(),this.notify("selection-cleared",{selectedItems:new Set})}isSelected(e){return this.selectedItems.has(e)}getSelection(){return new Set(this.selectedItems)}setCheckboxState(e,t){const s=this.container.querySelector(`[data-upload-id="${e}"]`),i=s?.querySelector(this.selectors.checkbox);i&&i.checked!==t&&(i.checked=t)}updateSelectionUI(){if(!this.lastClicked||!this.ui.count)return;const e=this.selectedItems.size;let t=this.lastClicked.querySelector(this.selectors.bulkControls);t&&(t.hidden=0===e);let s=this.lastClicked.querySelector(this.selectors.count);if(s){const t=1===e?"item":"items";s.textContent=0===e?"":`{ ${e} ${t} selected }`,s.hidden=0===e}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.container.removeEventListener("change",this.changeHandler),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("keydown",this.keyHandler),this.subscribers.clear(),this.selectedItems.clear()}};
\ No newline at end of file
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index e34308d..282d0da 100644
--- a/assets/js/min/navigation.min.js
+++ b/assets/js/min/navigation.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"]');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&this.toggleNav(!1,this.openNav);let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})();
\ No newline at end of file
diff --git a/assets/js/min/queue.min.js b/assets/js/min/queue.min.js
index 719e968..a589cbf 100644
--- a/assets/js/min/queue.min.js
+++ b/assets/js/min/queue.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return console.log("Added to Queue: ",t),this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0!==e.length){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e){try{this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data));const t=`${this.config.apiBase}${e.endpoint}`;let s;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),s=e.data):(s=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const i=await fetch(t,{method:e.method,headers:e.headers,body:s}),a=await i.json();if(!i.ok||!1===a.success)throw new Error(a.message||`HTTP ${i.status}`);a.id&&e.id!==a.id?e=await this.handleServerMerge(e,a):(e.status=a.status||"pending",e.serverData=a,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}getOperationIds(e){return e.map((e=>e.id))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"retry")}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}initUI(){this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}_updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),l=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const d=t.querySelector(".actions");d&&this.updateActionButtons(e,d)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(e={}){if(this.canUpdateUI=!0,this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...e},this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.user=window.auth.getUser(),!this.user)return console.log("Queue: User not logged in, queue disabled"),this.store=null,void(this.canUpdateUI=!1);this.headers={"X-WP-Nonce":window.auth.getNonce(),...e.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError;const t=window.jvbStore.register("queue",{storeName:"queue",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,delayFetch:!1});this.store=t.queue,this.classes=["offline","synced","pending"],this.initUI(),this.initListeners(),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.updateUI=()=>window.debouncer.schedule("queue-ui-update",this._updateUI.bind(this),100),this.initQueue()}async initQueue(){this.maybeStartPolling()||this.updateStatusPanel("synced"),this.store.subscribe(((e,t)=>{switch(e){case"data-loaded":case"items-saved":this.maybeStartPolling(),this.updateUI();break;case"item-saved":console.log(t,"Item saved data"),t.previousItem&&t.previousItem.status!==t.item.status&&this.handleOperationStatusChange(t.item,t.previousItem.status),this.maybeStartPolling();break;default:this.updateUI()}}))}maybeStartPolling(){return this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&(this.startPolling(),!0)}handleOperationStatusChange(e){switch(e.status){case"completed":console.log(e),this.notify("operation-completed",e);break;case"failed":this.notify("operation-failed",e);break;case"failed_permanent":this.notify("operation-failed-permanent",e)}}addToQueue(e){const t={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},sendNow:!1,canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...e};if(t.headers={...this.headers,...t.headers},!t.endpoint||!t.data)return console.error("Invalid operation queued: missing endpoint or data"),null;if(t.sendNow)return this.processOperation(t).then((()=>{})),this.store.clearCache(),window.debouncer.schedule("fastQueue",this.startPolling.bind(this),200),this.showQueue(),t.id;const s=Array.from(this.store.data.values()).filter((e=>"queued"===e.status&&e.endpoint===t.endpoint&&e.canMerge));if(s.length>0){const e=s[0];return e.data=window.deepMerge(e.data,t.data),e.timestamp=Date.now(),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}return this.store.clearCache(),this.setQueue(t),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}setQueue(e){this.store.save(e)}updateOperationStatus(e,t){let s=this.store.get(e);s&&(s.status=t,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(e){return this.store.get(e)}clearQueue(e){this.store.delete(e)}startActivityTracking(){if(!this.activityListeners){const e=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=e.map((e=>{const t=()=>this.resetActivityTimer();return document.addEventListener(e,t,{passive:!0}),{event:e,handler:t}}))}this.resetActivityTimer()}resetActivityTimer(){this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:e,handler:t})=>{document.removeEventListener(e,t)})),this.activityListeners=null)}hideQueue(){this.ui.panel.hidden=!0,this.ui.toggle.hidden=!0}showQueue(){this.ui.panel.hidden=!1,this.ui.toggle.hidden=!1}setProcessing(e){this.isProcessing=e,this.ui.toggle.classList.toggle("saving",e)}async processQueue(){if(this.isProcessing)return;const e=this.getOperationsByStatus("queued");if(0!==e.length){this.setProcessing(!0);for(const t of e)await this.processOperation(t);this.setProcessing(!1),this.stopActivityTracking(),this.maybeStartPolling()?this.showQueue():this.hideQueue()}else this.stopActivityTracking()}async processOperation(e,t=!1){try{t||(this.updateOperationStatus(e.id,"uploading"),e.data?._isFormData&&(e.data=await this.store.objectToFormData(e.data)));const s=`${this.config.apiBase}${e.endpoint}`;let i;e.data instanceof FormData?(e.data.append("id",e.id),e.data.append("user",this.user),i=e.data):(i=JSON.stringify({...e.data,id:e.id,user:this.user}),e.headers["Content-Type"]="application/json");const a=await fetch(s,{method:e.method,headers:e.headers,body:i}),n=await a.json();if(t&&(e.data={}),!a.ok||!1===n.success)throw new Error(n.message||`HTTP ${a.status}`);n.id&&e.id!==n.id?e=await this.handleServerMerge(e,n):(e.status=n.status||"pending",e.serverData=n,this.updateOperationStatus(e.id,e.status)),this.a11y.announce(`${e.title} sent to server for processing.`)}catch(t){console.error("Operation failed:",t),e.retries++,e.lastError=t.message,e.retries>=this.config.maxRetries?e.status="failed_permanent":(e.status="failed",e.nextRetry=Date.now()+1e3*Math.pow(2,e.retries)),this.updateOperationStatus(e.id,e.status),this.setQueue(e)}}async handleServerMerge(e,t){const s=this.getQueue(t.id);return s?(s.data=window.deepMerge(s.data,e.data),s.status=t.status||"pending",s.serverData=t,this.updateOperationStatus(s.id,s.status),this.removeOperationFromUI(e.id),this.clearQueue(e.id),s):(this.clearQueue(e.id),e.id=t.id,e.status=t.status||"pending",e.serverData=t,this.updateOperationStatus(e.id,e.status),e)}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{this.store.clearCache(),await this.store.fetch(),this.maybeStartPolling()||(this.stopPolling(),this.updateStatusPanel("synced"))}catch(e){console.error("Polling error:",e)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}getOperationIds(e){return e.map((e=>e.id))}async updateServerOperations(e,t){if(0===(e=(e=Array.isArray(e)?e:e.includes(",")?e.split(","):[e]).filter((e=>{let s=this.getQueue(e);return this.getAllowedActions(s.status).includes(t)}))).length)return;const s=["cancel","dismiss"].includes(t);s&&e.forEach((e=>this.removeOperationFromUI(e)));try{const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:e,action:t,user:window.auth.getUser()})});if(!i.ok)throw new Error(`${t} failed: ${i.status}`);const a=await i.json();if(!a.success)throw new Error(a.message||`${t} operation failed`);return e.forEach((e=>{let i=this.getQueue(e);this.notify(`${t}-operation`,i),s?this.clearQueue(e):(i.status="queued",i.retries=0,this.setQueue(i),this.updateOperationStatus(i.id,i.status))})),"retry"===t&&this.startActivityTracking(),this.updateUI(),a}catch(s){return await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:t,operationIds:e,itemCount:e.length},(()=>this.updateServerOperations(e,t))),{success:!1,error:s.message}}}getAllowedActions(e){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[e]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=e=>{if(this.isPolling||this.isProcessing)return e.preventDefault(),"You have unsaved changes in the queue. Proceed?"},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(e){if(e.target.closest(this.selectors.panel,this.selectors.toggle))if(e.target.closest(this.selectors.refreshButton))this.store.clearCache(),this.store.clearHttpHeaders(),this.store.fetch();else if(e.target.closest(this.selectors.clearButton)){const e=this.getOperationIds(this.getOperationsByStatus("completed"));e.length>0&&this.updateServerOperations(e,"dismiss")}else if(e.target.closest(this.selectors.retryButton)){const e=this.getOperationIds(this.getOperationsByStatus("failed"));e.length>0&&this.updateServerOperations(e,"retry")}else if(e.target.closest("[data-action]")){const t=e.target.closest("[data-action]"),s=t.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,t.dataset.action)}else if(e.target.closest(".filters [data-filter]")){const t=e.target.closest("[data-filter]").dataset.filter;this.setFilter(t)}}initUI(){this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui=window.uiFromSelectors(this.selectors),this.ui.panel||(this.canUpdateUI=!1)}_updateUI(){if(!this.canUpdateUI)return;const e=Array.from(this.store.data.values()),t=this.store.lastResponse?.queue_stats||{queued:0,localProcessing:0,uploading:0,pending:0,processing:0,completed:0,failed:0,failed_permanent:0};if(this.ui.count){const s=e.length-t.completed;this.ui.count.textContent=s>0?s:"",this.ui.count.style.display=s>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}this.ui.clearButton.disabled=0===this.getOperationsByStatus("completed").length,this.ui.retryButton.disabled=0===this.getOperationsByStatus("failed").length&&0===this.getOperationsByStatus("failed_permanent").length,Object.entries(this.ui.filters).forEach((([s,i])=>{const a="all"===s?e.length:t[s]||0,n=i.querySelector(".count");n&&(n.textContent=a>0?a:""),i.setAttribute("data-count",a)})),this.renderOperations()}getStatusLabel(e){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[e]||e}getItemMessage(e){if(e.message)return e.message;if(e.error_message)return e.error_message;switch(e.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return e.position?`Position ${e.position} in queue`:"In server queue";case"processing":return e.progress?`${e.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${e.lastError||"Unknown error"} (Retry ${e.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${e.lastError||"Unknown error"}`;default:return""}}calculateProgress(e){if(e.progress)return e.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[e.status]||0}renderOperations(){if(!this.ui.itemsContainer)return;const e=this.store.getFiltered();if(window.removeChildren(this.ui.itemsContainer),0===e.length){let e=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(e),this.a11y.announce("Nothing queued.")}else e.forEach((e=>{const t=this.createOperationUI(e);this.ui.itemsContainer.append(t)}))}createOperationUI(e){const t=window.getTemplate("queueItem");return t.dataset.id=e.id,this.updateOperationUI(e,t),t}updateOperationUI(e,t=null){t||(t=this.ui.itemsContainer?.querySelector(`[data-id="${e.id}"]`)),t||(t=this.createOperationUI(e)),this.statuses.forEach((e=>t.classList.remove(e))),t.classList.add(e.status);let s="";e.updated_at?s=window.formatTimeAgo(new Date(e.updated_at)):e.created_at&&(s=window.formatTimeAgo(new Date(e.created_at)));const i=this.calculateProgress(e),a=t.querySelector(".type"),n=t.querySelector(".status"),r=t.querySelector(".info .details"),o=t.querySelector(".info .time"),d=t.querySelector(".progress .fill");if(a&&(a.textContent=e.title),n){n.querySelector(".icon")?.remove();let t=this.getStatusLabel(e.status);n.title=t,n.prepend(window.getIcon(this.icons[e.status])),n.querySelector("span").textContent=t}r&&(r.textContent=this.getItemMessage(e)),o&&(o.textContent=s),d&&(d.style.width=`${i}%`);const l=t.querySelector(".actions");l&&this.updateActionButtons(e,l)}updateActionButtons(e,t){switch(window.removeChildren(t),e.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",t.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=e.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",t.appendChild(i),t.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",t.appendChild(n)}}removeOperationFromUI(e){const t=this.ui.itemsContainer?.querySelector(`[data-id="${e}"]`);t&&(t.style.opacity="0",t.style.transform="scale(0.9)",setTimeout((()=>t.remove()),300))}updateStatusPanel(e){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(e)&&this.ui.panel?.classList.add(e)}setFilter(e){Object.values(this.ui.filters).forEach((t=>{t&&t.classList.toggle("active",t.dataset.filter===e)})),"all"===e?this.store.clearFilters():this.store.setFilter("status",e)}getOperationsByStatus(e,t=!0){return Array.isArray(e)||"string"!=typeof e||(e=[e]),t?Array.from(this.store.data.values()).filter((t=>e.includes(t.status))):Array.from(this.store.data.values()).filter((t=>!e.includes(t.status)))}hasQueuedOperations(){return this.getOperationsByStatus("queued").length>0}subscribe(e){if(this.subscribers)return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbQueue=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploadGroups.min.js b/assets/js/min/uploadGroups.min.js
new file mode 100644
index 0000000..5683211
--- /dev/null
+++ b/assets/js/min/uploadGroups.min.js
@@ -0,0 +1 @@
+(()=>{class e{constructor(e){this.manager=e,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.groupElements=new Map,this.groupStoreReady=!1,this.selectors={container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info",display:".group-display",empty:".empty-group"},this.init()}async init(){const{groups:e}=window.jvbStore.register("uploads",[{storeName:"groups",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"pageUrl",keyPath:"pageUrl"}],TTL:6048e5,delayFetch:!0}]);this.groupStore=e,this.groupStore.subscribe((e=>{"data-loaded"===e&&(this.groupStoreReady=!0,this.checkForRecovery())})),this.manager.subscribe(this.handleManagerEvent.bind(this)),this.queue.subscribe(this.handleQueueEvent.bind(this)),this.initListeners(),this.manager.groups=this}handleManagerEvent(e,t){switch(e){case"field-registered":this.enhanceField(t.fieldId);break;case"files-processed":this.onFilesProcessed(t.fieldId);break;case"upload-removed":this.onUploadRemoved(t.uploadId,t.fieldId);break;case"field-state-updated":this.updateGroupCount(t.fieldId)}}handleQueueEvent(e,t){if("uploads/groups"!==t.endpoint)return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}}enhanceField(e){const t=this.manager.fieldElements.get(e);if(!t)return;const{element:o,config:a}=t;if("post_group"!==a.destination)return;const r=o.querySelector(this.selectors.display);r&&(t.ui.groups={display:r,container:o.querySelector(".item-grid.groups"),empty:o.querySelector(this.selectors.empty),groups:new Map}),this.initEmptyGroupDropZone(e),a.maxFiles=20}initEmptyGroupDropZone(e){const t=this.manager.fieldElements.get(e),o=t?.ui?.groups?.empty;o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",onEnd:t=>this.handleDrop(t,e)}))}onFilesProcessed(e){const t=this.manager.fieldElements.get(e);t&&("post_group"===t.config.destination&&t.ui.groups?.display&&(t.ui.groups.display.hidden=!1),this.initGroupSortables(e))}initGroupSortables(e){const t=this.manager.fieldElements.get(e);if(!t?.element)return;t.element.querySelectorAll(this.selectors.grid).forEach((t=>{if(t.sortableInstance)return;const o=t.closest(this.selectors.container)?.dataset.groupId;this.createSortableForGrid(t,e,o)}))}async onUploadRemoved(e,t){const o=this.manager.uploadStore.get(e);if(o?.groupId){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}}initListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this))}handleClick(e){const t=e.target.closest("[data-action]");if(!t)return;const o=t.dataset.action,a=this.manager.getFieldIdFromElement(t);switch(o){case"add-to-group":this.handleAddToGroup(a);break;case"delete-group":this.handleDeleteGroup(t);break;case"remove-from-group":this.handleRemoveFromGroup(t);break;case"upload":const o=this.manager.fieldElements.get(a);"post_group"===o?.config.destination&&(e.stopImmediatePropagation(),this.submitGroupedUploads(a))}}handleChange(e){const t=this.manager.getFieldIdFromElement(e.target);if(!t)return;const o=this.manager.fieldElements.get(t);if("post_group"!==o?.config.destination)return;const a=e.target.closest(this.selectors.container);a&&this.handleGroupMetaChange(e.target,a)}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const a=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>this.manager.syncCheckboxToSortable(e.item,!0),onDeselect:e=>this.manager.syncCheckboxToSortable(e.item,!1),onAdd:e=>this.manager.updateSortableState(e.to),onRemove:e=>this.manager.updateSortableState(e.from)});e.sortableInstance=a;const r=o?`${t}-group-${o}`:`${t}-preview`;return this.manager.sortableInstances.set(r,a),a}async handleDrop(e,t){const o=e.to,a=e.from,r=e.items?.length>0?e.items:[e.item],s=r.map((e=>e.dataset.uploadId));if(o===a)return void this.handleReorder(e);const i=this.getDropTargetType(o);try{switch(i){case"empty-group":const e=await this.createGroup(t);if(!e)throw new Error("Group creation failed");for(const t of s)await this.addToGroup(t,e.grid);break;case"preview":for(const e of s)await this.removeFromGroup(e);break;case"group":for(const e of s)await this.addToGroup(e,o);break;default:throw new Error("Unknown drop target")}this.finalizeDrop(t,r.length,i)}catch(e){console.error("Drop error:",e);const o=this.manager.fieldElements.get(t);o?.ui?.preview&&r.forEach((e=>o.ui.preview.appendChild(e))),this.a11y.announce("An error occurred. Items returned to preview.")}this.manager.updateSortableState(o),a!==o&&this.manager.updateSortableState(a)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}finalizeDrop(e,t,o){const a={"empty-group":t>1?`Created group with ${t} items`:"Created group with item",preview:t>1?`Moved ${t} items to preview`:"Moved item to preview",group:t>1?`Moved ${t} items to group`:"Moved item to group"};this.a11y.announce(a[o]||"Items moved"),this.manager.selectionHandlers.get(e)?.clearSelection()}handleReorder(e){const t=e.to,o=t.dataset.groupId;if(o){const e=this.groupStore.get(o);if(e){const o=Array.from(t.querySelectorAll(".item:not(.sortable-ghost)")).map((e=>e.dataset.uploadId)).filter(Boolean);e.uploads=o,this.groupStore.save(e)}}this.a11y.announce("Item reordered")}getFieldGroups(e){return this.groupStore.getAll().filter((t=>t.fieldId===e))}async createGroup(e,t=null){const o=this.manager.fieldElements.get(e);if(!o)return null;const a={id:t=t||this.manager.generateId("group"),fieldId:e,pageUrl:window.location.href,uploads:[],fields:{}};await this.groupStore.save(a);const r=this.createGroupElement(t,e);if(!r)return null;const s=r.querySelector(this.selectors.grid);return this.groupElements.set(t,{element:r,grid:s,fieldId:e}),o.ui.groups?.container&&o.ui.groups.empty?o.ui.groups.container.insertBefore(r,o.ui.groups.empty):o.ui.groups?.container&&o.ui.groups.container.appendChild(r),this.addGroupSelectionHandler(e,t),s&&this.createSortableForGrid(s,e,t),this.updateGroupCount(e),{id:t,element:r,grid:s}}createGroupElement(e,t){const o=window.getTemplate("imageGroup");if(!o)return null;o.dataset.groupId=e,o.dataset.fieldId=t;const a=window.getTemplate("groupMetadata"),r=o.querySelector(".fields");if(r&&a){r.append(a);const s=r.querySelector('[name="post_title"]'),i=r.querySelector('[name="post_excerpt"]');s&&(s.id=`${e}_title`,s.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.manager.fieldElements.get(t);if(n?.config.content){const e=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const s=o.querySelector(this.selectors.grid);return s&&(s.dataset.groupId=e),o}async deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const a=this.groupStore.get(e);if(t&&a?.uploads)for(const e of a.uploads)await this.removeFromGroup(e);await this.groupStore.delete(e),o.element?.remove(),this.groupElements.delete(e);const r=`${o.fieldId}-group-${e}`;this.manager.sortableInstances.get(r)?.destroy(),this.manager.sortableInstances.delete(r);const s=`${o.fieldId}_${e}`;this.manager.selectionHandlers.get(s)?.destroy(),this.manager.selectionHandlers.delete(s),this.updateGroupCount(o.fieldId),this.a11y.announce("Group removed")}async addToGroup(e,t){const o=this.manager.uploadStore.get(e),a=this.manager.uploadElements.get(e);if(!o||!a)return;const r=t.dataset.groupId,s=this.groupStore.get(r);if(o.groupId&&o.groupId!==r){const t=this.groupStore.get(o.groupId);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length?await this.deleteGroup(o.groupId,!1):await this.groupStore.save(t))}o.groupId=r,s&&!s.uploads.includes(e)&&(s.uploads.push(e),await this.groupStore.save(s)),await this.manager.uploadStore.save(o),t.appendChild(a.element),a.location=t;const i=a.element.querySelector('[name="featured"]');i&&(i.hidden=!1,i.name=`${r}_featured`);const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1),this.manager.updateSortableState(t)}async removeFromGroup(e){const t=this.manager.uploadStore.get(e),o=this.manager.uploadElements.get(e);if(!t||!o)return;const a=this.manager.fieldElements.get(t.fieldId);if(!a?.ui?.preview)return;if(t.groupId){const o=this.groupStore.get(t.groupId);o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length?await this.deleteGroup(t.groupId,!1):await this.groupStore.save(o)),t.groupId=null,await this.manager.uploadStore.save(t)}a.ui.preview.appendChild(o.element),o.location=a.ui.preview;const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.manager.updateSortableState(a.ui.preview)}async handleGroupMetaChange(e,t){const o=t.dataset.groupId,a=this.groupStore.get(o);if(!a)return;let r=e.name;r.includes(o)&&(r=r.replace(`${o}_`,"").replace(`${o}[`,"").replace("]","")),a.fields[r]=e.value,await this.groupStore.save(a)}updateGroupCount(e){const t=this.manager.fieldElements.get(e);if(!t)return;const o=this.getFieldGroups(e).length;t.element.dataset.hasGroups=o>0}async handleAddToGroup(e){const t=this.manager.selected.get(e);if(t&&0!==t.size){const o=await this.createGroup(e);if(!o)return;for(const e of t)await this.addToGroup(e,o.grid);this.manager.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`)}else await this.createGroup(e)}async handleDeleteGroup(e){const t=e.closest(this.selectors.container);t&&confirm("Delete this group? Items will be moved back to the upload area.")&&await this.deleteGroup(t.dataset.groupId,!0)}async handleRemoveFromGroup(e){const t=e.closest("[data-upload-id]");t&&(await this.removeFromGroup(t.dataset.uploadId),this.a11y.announce("Item moved to preview"))}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.manager.selectionHandlers.has(o))return;const a=this.groupElements.get(t);if(!a?.element)return;const r=new window.jvbHandleSelection({container:a.element,ui:{selectAll:a.element.querySelector(this.selectors.selectAll),bulkControls:a.element.querySelector(this.selectors.actions),count:a.element.querySelector(this.selectors.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});r.subscribe(((t,o)=>{["item-selected","item-deselected","range-selected"].includes(t)&&this.manager.selected.set(e,o.selectedItems)})),this.manager.selectionHandlers.set(o,r)}async submitGroupedUploads(e){const t=this.manager.getFieldUploads(e),o=this.getFieldGroups(e),a=this.manager.fieldElements.get(e);if(0===t.length||!a)return;a.element.closest("details")?.removeAttribute("open"),document.body.classList.add("uploading");const r=new FormData,s=[],i=[];for(const e of o){const o=t.filter((t=>t.groupId===e.id)),a={images:[],fields:{...e.fields}};for(const e of o){const t=await this.manager.getBlobData(e.id);if(t){r.append("files[]",t),a.images.push({upload_id:e.id,index:i.length}),i.push(e.id);const o=this.manager.uploadElements.get(e.id),s=o?.element?.querySelector('[name*="featured"]');s?.checked&&(a.fields.featured=e.id)}}a.images.length>0&&s.push(a)}const n=t.filter((e=>!e.groupId));for(const e of n){const t=await this.manager.getBlobData(e.id);t&&(r.append("files[]",t),s.push({images:[{upload_id:e.id,index:i.length}],fields:{}}),i.push(e.id))}r.append("content",a.config.content),r.append("user",a.config.itemID),r.append("fieldId",e),r.append("posts",JSON.stringify(s)),r.append("upload_ids",JSON.stringify(i));const d={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${s.length} ${a.config.content}${s.length>1?"s":""}...`,popup:`Creating ${s.length} post${s.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(d);for(const o of t)o.operationId=e,o.status="queued",await this.manager.uploadStore.save(o),this.manager.updateUploadUI(o.id);return this.a11y.announce(`Creating ${s.length} post${s.length>1?"s":""}`),e}catch(t){throw this.error.log(t,{component:"UploadGroups",action:"submitGroupedUploads",fieldId:e}),t}}async handleOperationComplete(e,t){const o=e.result?.data||e.serverData?.data||[];for(const e of o){const t=this.manager.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",await this.manager.uploadStore.save(t),this.manager.updateUploadUI(e.upload_id))}if(!t)return;const a=this.manager.getFieldUploads(t),r=new Set;for(const e of a)"completed"===e.status&&(e.groupId&&r.add(e.groupId),await this.manager.clearUpload(e.id));for(const e of r)await this.groupStore.delete(e),this.groupElements.get(e)?.element?.remove(),this.groupElements.delete(e);this.manager.updateFieldState(t),this.a11y.announce("All uploads completed successfully"),document.body.classList.remove("uploading")}async handleOperationFailed(e,t){const o=e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data?.upload_ids||[],a="operation-failed-permanent"===e.status?"failed_permanent":"failed";for(const e of o)await this.manager.updateUploadStatus(e,a);t&&this.manager.updateFieldState(t),document.body.classList.remove("uploading")}checkForRecovery(){if(!this.manager.storesReady||!this.groupStoreReady)return;if(this.hasCheckedForRecovery)return;const e=this.manager.uploadStore.getAll();if(!e.some((e=>e.groupId))&&this.manager.hasCheckedForRecovery)return void(this.hasCheckedForRecovery=!0);this.hasCheckedForRecovery=!0,this.manager.hasCheckedForRecovery=!0;const t=e.filter((e=>!e.operationId&&["processed","processing","queued"].includes(e.status)));if(0===t.length)return;const o=this.groupUploadsByField(t);this.showRecoveryNotification(o)}groupUploadsByField(e){const t=new Map;return e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,{fieldId:e.fieldId,pageUrl:e.pageUrl,uploads:[],groups:new Map});const o=t.get(e.fieldId);if(o.uploads.push(e),e.groupId){const t=this.groupStore.get(e.groupId);t&&o.groups.set(e.groupId,t)}})),t}async showRecoveryNotification(e){let t=0,o=0;e.forEach((e=>{t+=e.uploads.length,o+=e.groups.size}));const a=window.getTemplate("restoreNotification");if(!a)return;const r=o>0?`${o} group${o>1?"s":""} with ${t} upload${t>1?"s":""} can be restored.`:`${t} upload${t>1?"s":""} can be recovered.`,s=a.querySelector(".restore-details");s&&(s.textContent=r);for(const[t,o]of e){const e=window.getTemplate("restoreField");if(!e)continue;const r=e.querySelector("h3");r&&(r.textContent=t);const s=e.querySelector(".item-grid.restore");for(const e of o.uploads){const o=await this.manager.getBlobData(e.id);if(!o)continue;const a=this.manager.createUploadElement({id:e.id,preview:this.manager.createPreviewUrl(o),meta:e.meta,subtype:this.manager.getSubtypeFromMime(o.type)});a.dataset.fieldId=t,s?.appendChild(a)}a.querySelector(".wrap")?.appendChild(e)}document.querySelector(".field.upload")?.appendChild(a);const i=document.querySelector("dialog.restore-uploads");i&&(this.restoreModal=new window.jvbModal(i),this.restoreSelection=new window.jvbHandleSelection({container:i,ui:{selectAll:i.querySelector("#select-all-restore"),count:i.querySelector(".selection-count")}}),this.restoreModal.handleOpen(),i.addEventListener("click",this.handleRestoreAction.bind(this)))}handleRestoreAction(e){const t=e.target.closest("[data-action]")?.dataset.action;switch(t){case"restore":this.handleRestoreSelected();break;case"restore-all":this.handleRestoreAll();break;case"clear-cache":this.handleClearCache()}}async handleRestoreSelected(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.dataset.fieldId})})),t.length>0&&await this.restoreUploads(t),this.cleanupRestoreModal()}async handleRestoreAll(){const e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{t.push({uploadId:e.dataset.uploadId,fieldId:e.dataset.fieldId})})),await this.restoreUploads(t),this.cleanupRestoreModal()}async restoreUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,o]of t)await this.restoreFieldUploads(e,o)}async restoreFieldUploads(e,t){let o=this.manager.fieldElements.get(e);if(!o){const t=document.querySelector(`[data-uploader="${e}"]`);t&&(this.manager.registerUploader(t),o=this.manager.fieldElements.get(e),this.enhanceField(e))}if(!o)return void console.warn(`Field ${e} not found for restoration`);o.ui.dropZone&&(o.ui.dropZone.hidden=!0),o.ui.groups?.display&&(o.ui.groups.display.hidden=!1);const a=this.getFieldGroups(e);for(const t of a)await this.restoreGroup(e,t);for(const o of t){const t=this.manager.uploadStore.get(o);t&&await this.restoreUploadElement(e,t)}this.manager.updateFieldState(e),this.manager.refreshSortable(e),this.manager.maybeLockUploads(e)}async restoreGroup(e,t){const o=await this.createGroup(e,t.id);if(o&&t.fields){const e=o.element.querySelector('[name*="post_title"]'),a=o.element.querySelector('[name*="post_excerpt"]');e&&t.fields.post_title&&(e.value=t.fields.post_title),a&&t.fields.post_excerpt&&(a.value=t.fields.post_excerpt)}}async restoreUploadElement(e,t){const o=this.manager.fieldElements.get(e);if(!o)return;const a=await this.manager.getBlobData(t.id);if(!a)return;const r=this.manager.createPreviewUrl(a),s=this.manager.createUploadElement({id:t.id,preview:r,meta:t.meta,subtype:this.manager.getSubtypeFromMime(a.type)});let i;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){i=e.grid;const o=s.querySelector('[name="featured"]');o&&(o.hidden=!1,o.name=`${t.groupId}_featured`)}else i=o.ui.preview}else i=o.ui.preview;i.appendChild(s),this.manager.uploadElements.set(t.id,{element:s,preview:r,location:i}),t.status="processed",await this.manager.uploadStore.save(t)}handleClearCache(){confirm("Discard these uploads?")&&(this.manager.uploadStore.clear(),this.groupStore.clear(),this.cleanupRestoreModal())}cleanupRestoreModal(){this.restoreModal&&(this.restoreModal.handleClose(),this.restoreSelection?.destroy(),this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null,this.restoreSelection=null)}destroy(){this.groupElements.forEach(((e,t)=>{const o=`${e.fieldId}-group-${t}`;this.manager.sortableInstances.get(o)?.destroy(),this.manager.sortableInstances.delete(o)})),this.groupElements.clear()}}document.addEventListener("DOMContentLoaded",(()=>{const t=()=>{window.jvbUploads&&!window.jvbUploadGroups&&(window.jvbUploadGroups=new e(window.jvbUploads))};window.jvbUploads?t():window.auth?.subscribe((e=>{"auth-loaded"===e&&setTimeout(t,50)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/uploader.min.js b/assets/js/min/uploader.min.js
index 3966c88..467fd2b 100644
--- a/assets/js/min/uploader.min.js
+++ b/assets/js/min/uploader.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStoreReady=!1,this.uploadStoreReady=!1,this.hasCheckedForUploads=!1;const{fields:e,uploads:t}=window.jvbStore.register("uploads",[{storeName:"fields",keyPath:"id",indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],TTL:6048e5,delayFetch:!0},{storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}],delayFetch:!0}]);this.fieldStore=e,this.uploadStore=t,window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.uploadElements=new Map,this.fieldElements=new Map,this.groupElements=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.sortableInstances=new Map,this.initWorker(),this.subscribers=new Set,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){this.initListeners(),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const o=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;switch(e){case"cancel-operation":o&&this.handleOperationCancelled(o);break;case"operation-status":o&&this.updateFieldStatus(o,t.status);break;case"operation-complete":this.handleOperationComplete(t,o);break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,o)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}scanFields(e,t){console.log(t,"autoUpload");e.querySelectorAll(this.selectors.field.field).forEach((e=>this.registerUploader(e,t)))}registerUploader(e,t){const o=this.determineFieldId(e),s=this.extractFieldConfig(e,t),a=this.buildFieldUI(e);console.log(s,"registering with config");const r={id:o,config:s,uploads:new Set,groups:[],state:"ready",timestamp:Date.now()};return this.fieldStore.save(r),this.fieldElements.set(o,{element:e,ui:a,config:s}),e.dataset.uploader=o,this.addFieldSelectionHandler(o),"single"!==s.type&&this.initSortable(o),o}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},o=e.querySelector(".group-display");return o&&(t.groups={display:o,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initSortable(e){if(!window.Sortable)return;!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0);const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((t=>{const o=t.classList.contains("group")?t.closest(".upload-group")?.dataset.groupId:null;this.createSortableForGrid(t,e,o)}));const o=t.element.querySelector(".empty-group");o&&!o.sortableInstance&&(o.sortableInstance=new Sortable(o,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:e,pull:!1,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:t=>this.handleDrop(t,e)}))}syncSortableSelection(e,t){this.sortableInstances.forEach(((o,s)=>{if(s.startsWith(e)){o.el.querySelectorAll(".item").forEach((e=>{const o=e.dataset.uploadId;t.has(o)?Sortable.utils.select(e):Sortable.utils.deselect(e)}))}}))}handleDrop(e,t){const o=e.to,s=e.from,a=e.items?.length>0?e.items:[e.item],r=a.map((e=>e.dataset.uploadId));switch(this.getDropTargetType(o)){case"empty-group":this.handleDropToEmptyGroup(a,r,t);break;case"preview":default:this.handleDropToPreview(a,r,t);break;case"group":this.handleDropToGroup(a,r,o,s,t)}this.updateSortableState(o),s!==o&&this.updateSortableState(s)}getDropTargetType(e){return e.classList.contains("empty-group")?"empty-group":e.classList.contains("preview")?"preview":e.classList.contains("group")?"group":"unknown"}handleDropToGroup(e,t,o,s,a){try{if(o===s)return void this.handleReorder({to:o,items:e});t.forEach((e=>{this.addToGroup(e,o,!1)})),this.schedulePersistance(a);const r=e.length>1?`Moved ${e.length} items to group`:"Moved item to group";this.a11y.announce(r);const i=this.selectionHandlers.get(a);i?.clearSelection()}catch(t){this.handleDropError(e,a,t)}}handleDropToPreview(e,t,o){try{t.forEach((e=>{this.removeFromGroup(e)})),this.schedulePersistance(o);const s=e.length>1?`Moved ${e.length} items to preview`:"Moved item to preview";this.a11y.announce(s);const a=this.selectionHandlers.get(o);a?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}handleDropError(e,t,o,s="An error occurred"){console.error("Drop error:",o);const a=this.fieldElements.get(t);a?.ui?.preview&&e.forEach((e=>a.ui.preview.appendChild(e))),this.a11y.announce(`${s}. Items returned to preview.`)}handleDropToEmptyGroup(e,t,o){try{const s=this.createGroup(o);if(!s)return void this.handleDropError(e,o,new Error("Group creation failed"),"Failed to create group");e.forEach(((e,o)=>{s.grid.appendChild(e),this.addToGroup(t[o],s.grid,!1)})),this.schedulePersistance(o);const a=e.length>1?`Created group with ${e.length} items`:"Created group with item";this.a11y.announce(a);const r=this.selectionHandlers.get(o);r?.clearSelection()}catch(t){this.handleDropError(e,o,t)}}updateSortableState(e){const t=e?.sortableInstance;t&&t.option("disabled",!1)}refreshSortable(e){const t=this.fieldElements.get(e);if(!t)return;t.element.querySelectorAll(".item-grid.preview, .item-grid.group").forEach((e=>this.updateSortableState(e)))}handleReorder(e){const t=e.to,o=t.closest(".field, .upload");if(!o)return;let s=Array.from(t.querySelectorAll(".item:not(.sortable-ghost):not(.sortable-clone)")).map((e=>e.dataset.uploadId)).filter((e=>e)),a=o.querySelector('input[type="hidden"]');a&&s.length>0&&(a.value=s.join(","));const r=this.getFieldIdFromElement(t);if(r){const e=this.getFieldData(r);if(t.classList.contains("group")){const o=t.dataset.groupId,a=e?.groups?.find((e=>e.id===o));a&&(a.uploads=s)}this.schedulePersistance(r)}this.a11y.announce("Item reordered"),o.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex,items:s},bubbles:!0}))}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const o=Array.from(e.dataTransfer.files);if(0===o.length)return;const s=this.getFieldIdFromElement(t);s&&(this.processFiles(s,o),this.a11y.announce(`${o.length} file(s) dropped for upload`))}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const o=Array.from(e.target.files);o.length>0&&t&&this.processFiles(t,o)}if(t){const o=this.getFieldData(t);if(!o.config.autoUpload)return;"post_group"===o?.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e)}}async processFiles(e,t){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups?.display&&(s.ui.groups.display.hidden=!1);const a=t.length;let r=0;this.updateUploadProgress(e,0,a,"Processing files...");const i=Array.from(t).map((async t=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,n={id:i,attachmentId:null,fieldId:e,status:"local_processing",groupId:null,meta:{originalName:t.name,size:t.size,type:t.type}};await this.uploadStore.save(n);const l=this.createPreviewUrl(t),d=t.type.startsWith("image/")?await this.processImage(t,o.config.subtype):t;this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),await this.saveBlobData(i,d||t);const c=this.getSubtypeFromMime(t.type),u=this.createUploadElement({id:i,preview:l,meta:n.meta,subtype:c},"post_group"===o.config.destination);s.ui.preview&&(s.ui.preview.appendChild(u),this.uploadElements.set(i,{element:u,preview:l,location:s.ui.preview}));const p=this.uploadStore.get(i);return p&&(p.status="processed",await this.uploadStore.save(p)),o.uploads.add(i),await this.saveFieldData(o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),setTimeout((()=>this.showUploadProgress(i,!1)),1e3),i}catch(o){return console.error("Error processing file:",t.name,o),r++,this.updateUploadProgress(e,r,a,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),this.refreshSortable(e),o.config.autoUpload&&"post_group"!==o.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}async processImage(e,t){const o=this.worker.settings.timeout;return new Promise(((s,a)=>{let r,i=!1;r=setTimeout((()=>{i||(i=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),a(new Error(`Processing timeout for ${e.name}`)))}),o),this.worker.tasks.set(t,{file:e,timeoutId:r}),this.handleProcess(e,t).then((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),s(e))})).catch((e=>{i||(i=!0,clearTimeout(r),this.worker.tasks.delete(t),a(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const o=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,o,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,o,.85)}async processOnMainThread(e,t,o){return new Promise(((s,a)=>{const r=new Image,i=document.createElement("canvas"),n=i.getContext("2d");let l=null;const d=()=>{r.onload=null,r.onerror=null,l&&(URL.revokeObjectURL(l),l=null),i.width=1,i.height=1,n.clearRect(0,0,1,1)};r.onload=()=>{try{const{width:l,height:c}=this.calculateOptimalDimensions(r,t);i.width=l,i.height=c,n.imageSmoothingEnabled=!0,n.imageSmoothingQuality="high",n.drawImage(r,0,0,l,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,o);i.toBlob((t=>{if(d(),t){const o=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});s(o)}else a(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),a(new Error(`Canvas processing failed: ${e.message}`))}},r.onerror=()=>{d(),a(new Error(`Failed to load image: ${e.name}`))};try{l=this.createPreviewUrl(e),r.src=l}catch(e){d(),a(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,o,s){return new Promise(((a,r)=>{if(!this.worker.worker)return void r(new Error("Worker not available"));const i=`${t}_${Date.now()}`,n=t=>{if(t.data.messageId===i)if(this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),t.data.success){const o=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});a(o)}else r(new Error(t.data.error||"Worker processing failed"))},l=e=>{this.worker.worker.removeEventListener("message",n),this.worker.worker.removeEventListener("error",l),r(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",n),this.worker.worker.addEventListener("error",l),this.worker.worker.postMessage({messageId:i,file:e,maxDimension:o,quality:s,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n\t\t\t\tself.onmessage = async function(e) {\n\t\t\t\t\tconst { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst bitmap = await createImageBitmap(file);\n\t\t\t\t\t\tconst scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n\t\t\t\t\t\tconst width = Math.round(bitmap.width * scale);\n\t\t\t\t\t\tconst height = Math.round(bitmap.height * scale);\n\t\t\t\t\t\tconst canvas = new OffscreenCanvas(width, height);\n\t\t\t\t\t\tconst ctx = canvas.getContext('2d');\n\t\t\t\t\t\tctx.imageSmoothingEnabled = true;\n\t\t\t\t\t\tctx.imageSmoothingQuality = 'high';\n\t\t\t\t\t\tctx.drawImage(bitmap, 0, 0, width, height);\n\t\t\t\t\t\tbitmap.close();\n\t\t\t\t\t\tconst blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });\n\t\t\t\t\t\tself.postMessage({ messageId, success: true, blob: blob, format: outputFormat });\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tself.postMessage({ messageId, success: false, error: error.message });\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t"],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:o,height:s}=e;if(o<=t&&s<=t)return{width:o,height:s};const a=Math.min(t/o,t/s);return{width:Math.round(o*a),height:Math.round(s*a)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}async submitUploads(e){const t=this.getFieldData(e);this.fieldElements.get(e);if(!t?.uploads||0===t.uploads.size)return;let o=Array.from(t.uploads);if(0===o.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const s=this.getFieldGroups(e);if(0===s.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const a=[],r=new FormData;let i=[];for(const e of s){const t={images:[],fields:{}};for(let[o,s]of Object.entries(e.changes))t.fields[o]=s;const s=o.filter((t=>{const o=this.uploadStore.get(t);return o?.groupId===e.id}));for(const e of s){const o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length},a=this.uploadElements.get(e),n=a?.element?.querySelector('[name="featured"]');n?.checked&&(t.fields.featured=e),t.images.push(s),i.push(e)}}a.push(t)}const n=o.filter((e=>{const t=this.uploadStore.get(e);return!t?.groupId}));for(const e of n){const t={images:[],fields:{}},o=await this.getBlobData(e);if(o){r.append("files[]",o);const s={upload_id:e,index:i.length};t.images.push(s),i.push(e)}a.push(t)}r.append("content",t.config.content),r.append("user",t.config.itemID),r.append("posts",JSON.stringify(a)),r.append("upload_ids",JSON.stringify(i));const l={endpoint:"uploads/groups",method:"POST",data:r,title:`Creating ${a.length} ${t.config.content}${a.length>1?"s":""} from uploads...`,popup:`Creating ${a.length} post${a.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(l);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),this.a11y.announce(`Creating ${a.length} post${a.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}}async queueUpload(e){const t=this.getFieldData(e);if(!t?.uploads||0===t.uploads.size)return;const o=Array.from(t.uploads),s=this.prepareUploadData(t,o);this.a11y.announce("Queuing for upload");const a={endpoint:"uploads",method:"POST",data:s,title:`Uploading ${o.length} file${o.length>1?"s":""} to server...`,popup:`Uploading ${o.length} file${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{const e=await this.queue.addToQueue(a);return o.forEach((t=>{const o=this.uploadStore.get(t);o&&(o.operationId=e,o.status="queued",this.uploadStore.save(o),this.updateUploadStatus(t,"queued"))})),t.operationId=e,await this.saveFieldData(t),e}catch(e){throw e}}async prepareUploadData(e,t){const o=new FormData;o.append("content",e.config.content),o.append("mode",e.config.mode),o.append("field_name",e.config.name),o.append("fieldId",e.id),o.append("field_type",e.config.type),o.append("subtype",e.config.subtype),o.append("item_id",e.config.itemID),o.append("destination",e.config.destination||"meta");let s=[];const a=t.map((async e=>{const t=this.uploadStore.get(e);if(!t)return;const a=await this.getBlobData(e);a&&(o.append("files[]",a),s.push(t.id))}));return await Promise.all(a),o.append("upload_ids",JSON.stringify(s)),o}async queueUploadMeta(e){const t=this.getUploadIdFromElement(e.target),o=this.uploadStore.get(t);if(!o)return;if(!this.getFieldData(o.fieldId))return;let s={};s[e.target.name]=e.target.value,o.meta={...o.meta,...s},await this.uploadStore.save(o);let a={};a[o.attachmentId??o.id]=o.meta;const r={endpoint:"uploads/meta",method:"POST",data:a,title:"Updating meta",canMerge:!0,headers:{action_nonce:window.auth.getNonce("dash")}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:o.id})}}async handleOperationComplete(e,t){if((e.result?.data||e.serverData?.data||[]).forEach((e=>{const t=this.uploadStore.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploadStore.save(t),this.updateUploadStatus(e.upload_id,"completed"))})),!t)return;const o=this.getFieldData(t);if(!o)return;const s=Array.from(o.uploads).filter((e=>{const t=this.uploadStore.get(e);return"completed"===t?.status}));for(const e of s)await this.clearUpload(e,!1),o.uploads.delete(e);0===o.uploads.size?(await this.clearFieldFromStores(t),this.a11y.announce("All uploads completed successfully")):await this.saveFieldData(o),this.updateFieldState(t)}handleOperationFailed(e,t){(e.data instanceof FormData?JSON.parse(e.data.get("upload_ids")||"[]"):e.data.upload_ids||[]).forEach((t=>{const o=this.uploadStore.get(t);o&&(o.status="operation-failed-permanent"===e.status?"failed_permanent":"failed",this.uploadStore.save(o),this.updateUploadStatus(t,o.status))})),t&&this.updateFieldState(t)}async handleOperationCancelled(e){const t=this.getFieldData(e);if(!t)return;const o=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of o)await this.clearUpload(e,!1);await this.clearFieldFromStores(e),this.updateFieldState(e),this.a11y.announce("Upload cancelled")}getFieldGroups(e){const t=this.getFieldData(e);return t?.groups?t.groups.map((e=>({id:e.id,uploads:e.uploads||[],changes:e.changes||{}}))):[]}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const o=e.closest(".item");o&&t.push({uploadId:o.dataset.uploadId,fieldId:o.dataset.fieldId})})),t}async restoreSelectedUploads(e){const t=new Map;e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)}));for(const[e,o]of t.entries()){const t=this.fieldStore.get(e);t&&(t.uploads=o,await this.restoreField(t))}}async restoreField(e){const{config:t,context:o,uploads:s,groups:a,id:r}=e;o?.modalType&&await this.openModalForRestore(o);let i=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!i){const e=`${t.content}_${t.itemID}_${t.name}`;i=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!i)return void console.warn(`Field ${t.name} not found for restoration`,t);let n=i.dataset.uploader;n&&this.fieldElements.has(n)||(n=this.registerUploader(i));const l=this.fieldElements.get(n),d=this.getFieldData(n);if(!l||!d)return void console.error("Failed to register field for restoration");d.state=e.state||"ready",l.ui||(l.ui=this.buildFieldUI(i)),l.ui.groups?.display&&(l.ui.groups.display.hidden=!1),l.ui.dropZone&&(l.ui.dropZone.hidden=!0),a&&a.length>0&&await this.restoreGroups(n,a);const c=s instanceof Set?Array.from(s):Array.isArray(s)?s:[];for(const e of c){const t=this.uploadStore.get(e);t&&await this.restoreUpload(n,t)}await this.saveFieldData(d),this.updateFieldState(n),this.maybeLockUploads(n),this.refreshSortable(n),console.log(t),t.autoUpload&&"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(n)}async restoreUpload(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(!o||!s)return void console.error("Field not found for upload restoration:",e);const a=await this.getBlobData(t.id);if(!a)return void console.warn("Blob data not found for upload:",t.id);const r=this.createPreviewUrl(a),i=this.getSubtypeFromMime(a.type),n=this.createUploadElement({id:t.id,preview:r,meta:t.meta||{originalName:a.name,size:a.size,type:a.type},subtype:i},"post_group"===s.config.destination);let l;if(t.groupId){const e=this.groupElements.get(t.groupId);if(e?.grid){l=e.grid;const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads||(o.uploads=[]),o.uploads.includes(t.id)||o.uploads.push(t.id))}else l=o.ui.preview,t.groupId=null}else l=o.ui.preview;l?l.appendChild(n):o.ui.preview&&(o.ui.preview.appendChild(n),l=o.ui.preview),this.uploadElements.set(t.id,{element:n,preview:r,location:l}),s.uploads||(s.uploads=new Set),s.uploads.add(t.id),t.status="processed",await this.uploadStore.save(t),l&&this.updateSortableState(l)}async restoreGroups(e,t){const o=this.fieldElements.get(e),s=this.getFieldData(e);if(o&&s){for(const o of t){const t=this.createGroup(e,o.id);if(!t){console.warn("Failed to create group:",o.id);continue}const a=s.groups?.find((e=>e.id===o.id));if(a&&(o.changes&&(a.changes={...o.changes}),o.uploads&&(a.uploads=[...o.uploads]),o.changes)){const e=t.element.querySelector('[name*="post_title"]'),s=t.element.querySelector('[name*="post_excerpt"]');e&&o.changes.post_title&&(e.value=o.changes.post_title),s&&o.changes.post_excerpt&&(s.value=o.changes.post_excerpt)}}await this.saveFieldData(s)}else console.error("Field not found for group restoration:",e)}async openModalForRestore(e){if(!e)return;const{modalType:t,itemId:o}=e;let s=null;switch(t){case"create":s=document.querySelector('[data-action="create"]');break;case"edit":o&&(s=document.querySelector(`[data-action="edit"][data-id="${o}"]`));break;case"bulkEdit":s=document.querySelector('[data-action="bulk-edit"]')}s?(s.click(),await new Promise((e=>setTimeout(e,300)))):console.warn("Modal trigger not found for restoration:",e)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const o=t<0?0:t,s=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,s)).toFixed(o))+" "+["Bytes","KB","MB","GB"][s]}async clearUpload(e,t=!0){const o=this.uploadElements.get(e);if(o&&(this.revokePreviewUrl(o.preview),o.element)){const e=o.element.dataset.previewUrl;this.revokePreviewUrl(e),delete o.element.dataset.previewUrl}if(this.uploadElements.delete(e),await this.uploadStore.delete(e),t){const t=this.uploadStore.get(e);t?.fieldId&&await this.schedulePersistance(t.fieldId)}}async clearFieldFromStores(e){const t=this.getFieldData(e);if(t?.uploads){const e=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const t of e)await this.uploadStore.delete(t)}await this.fieldStore.delete(e)}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}updateFieldState(e){const t=this.fieldElements.get(e),o=this.getFieldData(e);if(!t||!o)return;const s=t.element,a=o.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=a>0?"true":"false",s.dataset.uploadCount=a.toString(),s.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${a} item${1!==a?"s":""}`)}updateUploadProgress(e,t,o,s){const a=this.fieldElements.get(e);if(!a?.ui?.progress?.progress)return;const r=a.ui.progress,i=o>0?t/o*100:0;r.fill&&(r.fill.style.width=`${i}%`),r.text&&(r.text.textContent=s),r.count&&(r.count.textContent=`${t}/${o}`),r.progress.hidden=t===o}updateFieldStatus(e,t){const o=this.getFieldData(e);o&&(o.state=t,this.saveFieldData(o))}updateUploadStatus(e,t){const o=this.uploadStore.get(e);o&&(o.status=t,this.uploadStore.save(o),this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploadElements.get(e),o=this.uploadStore.get(e);if(!o||!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${o.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(o.status),o.status)}showUploadProgress(e,t=!0){const o=this.uploadElements.get(e);if(!o?.element)return;const s=o.element.querySelector(".progress");s&&(t?(s.style.removeProperty("animation"),s.hidden=!1):(s.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{s.hidden=!0}),300)))}updateUploadItemProgress(e,t,o=null){const s=this.uploadElements.get(e);if(!s?.element)return;const a=s.element.querySelector(".progress");if(!a)return;const r=a.querySelector(".fill"),i=a.querySelector(".details"),n=a.querySelector(".icon");r&&(r.style.width=`${t}%`),o&&i&&(i.textContent=this.getStatusText(o)),o&&n&&(n.innerHTML=this.getStatusIcon(o).outerHTML)}maybeLockUploads(e){const t=this.fieldElements.get(e),o=this.getFieldData(e);if(!t?.ui?.dropZone||!o)return;const s=o.uploads?.size||0,a="post_group"===o.config.destination?20:o.config?.maxFiles||999;t.ui.dropZone.hidden=s>=a,t.element.classList.toggle("at-max-uploads",s>=a),"post_group"===o.config.destination&&s>=a&&this.a11y.announce("Maximum of 20 uploads reached. Please submit current uploads before adding more.")}createSortableForGrid(e,t,o=null){if(!e||e.sortableInstance)return;const s=new Sortable(e,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected-for-drag",avoidImplicitDeselect:!0,group:{name:t,pull:!0,put:!0},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>this.handleDrop(e,t),onSelect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&!t.checked&&(t.checked=!0,t.dispatchEvent(new Event("change",{bubbles:!0})))},onDeselect:e=>{const t=e.item.querySelector('[name*="select-item"]');t&&t.checked&&(t.checked=!1,t.dispatchEvent(new Event("change",{bubbles:!0})))},onAdd:e=>this.updateSortableState(e.to),onRemove:e=>this.updateSortableState(e.from)});e.sortableInstance=s;const a=o?`${t}-group-${o}`:`${t}-preview`;return this.sortableInstances.set(a,s),s}createGroup(e,t=null){const o=this.getFieldData(e),s=this.fieldElements.get(e);if(!o||!s)return null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const a=this.createGroupElement(t,e);if(!a)return null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,a),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(a,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(a);const r=a.querySelector(".item-grid.group");this.groupElements.set(t,{element:a,grid:r,fieldId:e}),o.groups||(o.groups=[]);return o.groups.find((e=>e.id===t))||(o.groups.push({id:t,uploads:[],changes:{}}),this.saveFieldData(o)),this.addGroupSelectionHandler(e,t),r&&this.createSortableForGrid(r,e,t),{id:t,element:a,grid:r}}createGroupElement(e,t){let o=window.getTemplate("imageGroup");if(!o)return;o.dataset.groupId=e,o.dataset.fieldId=t;let s=window.getTemplate("groupMetadata");const a=o.querySelector(".fields");if(a&&s){a.append(s);const r=a.querySelector('[name="post_title"]'),i=a.querySelector('[name="post_excerpt"]');r&&(r.id=`${e}_title`,r.name=`${e}[post_title]`),i&&(i.id=`${e}_excerpt`,i.name=`${e}[post_excerpt]`);const n=this.getFieldData(t);if(n&&""!==n.config.content){let e=o.querySelector("summary");e&&(e.textContent=n.config.content+" Fields")}}else o.querySelector("details")?.remove();const r=o.querySelector(".item-grid.group");return r&&(r.dataset.groupId=e),o}deleteGroup(e,t=!0){const o=this.groupElements.get(e);if(!o)return;const s=this.getFieldData(o.fieldId);if(!s)return;const a=s.groups?.find((t=>t.id===e));let r=!0;t&&a?.uploads?.length>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&a?.uploads&&a.uploads.forEach((e=>{this.removeFromGroup(e)})),s.groups&&(s.groups=s.groups.filter((t=>t.id!==e)),this.saveFieldData(s)),o.element&&(o.element.remove(),this.a11y.announce("Group removed")),this.groupElements.delete(e);const i=`${o.fieldId}-group-${e}`,n=this.sortableInstances.get(i);n?.destroy&&n.destroy(),this.sortableInstances.delete(i),this.schedulePersistance(o.fieldId)}addToGroup(e,t=null,o=!0){const s=this.uploadStore.get(e),a=this.uploadElements.get(e);if(!s||!a)return;const r=this.getFieldData(s.fieldId),i=this.fieldElements.get(s.fieldId);if(!r||!i)return;if(!t&&a.location===i.ui.preview||t===a.location)return;if(s.groupId){const t=r.groups?.find((e=>e.id===s.groupId));t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&this.deleteGroup(s.groupId))}const n=a.element.querySelector('[name*="select-item"]');n&&(n.checked=!1);let l=a.element.querySelector('[name="featured"]');if(l&&(l.hidden=!t),!t||t.classList.contains("preview"))t=i.ui.preview,s.groupId=null;else{const o=t.dataset.groupId;l&&(l.name=o+"_"+l.name);const a=r.groups?.find((e=>e.id===o));a&&(a.uploads||(a.uploads=[]),a.uploads.push(e),s.groupId=o)}a.location=t,t.append(a.element),this.uploadStore.save(s),o&&this.saveFieldData(r),this.updateSortableState(t),a.location&&a.location!==t&&this.updateSortableState(a.location)}removeFromGroup(e){const t=this.uploadStore.get(e),o=this.uploadElements.get(e);if(!t||!o)return;const s=this.getFieldData(t.fieldId),a=this.fieldElements.get(t.fieldId);if(!s||!a)return;if(t.groupId){const o=s.groups?.find((e=>e.id===t.groupId));o&&(o.uploads=o.uploads.filter((t=>t!==e)),0===o.uploads.length&&this.deleteGroup(t.groupId,!1)),t.groupId=null}a.ui?.preview&&(a.ui.preview.appendChild(o.element),o.location=a.ui.preview);const r=o.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1),this.uploadStore.save(t),this.updateSortableState(a.ui.preview)}removeUpload(e,t){const o=this.getFieldData(e),s=this.uploadStore.get(t),a=this.uploadElements.get(t);if(!o||!s)return;if(o.uploads?.delete(t),s.groupId){const e=o.groups?.find((e=>e.id===s.groupId));e&&(e.uploads=e.uploads.filter((e=>e!==t)),0===e.uploads.length&&this.deleteGroup(s.groupId))}a?.element?.remove(),this.clearUpload(t),this.saveFieldData(o),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(e);r&&r.deselect(t),this.a11y.announce("Upload removed")}handleGroupMetaChange(e){const t=this.getGroupFromElement(e);if(!t)return;const o=this.getFieldData(t.fieldId),s=o?.groups?.find((e=>e.id===t.element.dataset.groupId));if(!s)return;s.changes||(s.changes={});let a=e.name;a.includes("group")&&(a=a.replace(`${s.id}_`,"").replace(`${s.id}[`,"").replace("]","")),s.changes[a]=e.value,this.saveFieldData(o),this.schedulePersistance(t.fieldId)}handleAction(e){const t=e.dataset.action,o=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":const t=this.fieldElements.get(o);t&&(t.element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(o));break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),o=t?.dataset.uploader;if(!o)return;const s=this.selected.get(o);if(s&&0!==s.size){const e=this.createGroup(o);if(!e)return;s.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(o);t?.clearSelection(),this.a11y.announce(`Created group with ${s.size} items`)}else this.createGroup(o);this.schedulePersistance(o)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const o=t.dataset.groupId,s=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(o),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(s)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const o=t.dataset.uploadId,s=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(s,o),this.a11y.announce("Item removed"),this.schedulePersistance(s))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fieldElements.get(e);if(!t?.element)return;const o=new window.jvbHandleSelection({container:t.element,ui:{selectAll:t.element.querySelector('[name="select-all-uploads"]'),bulkControls:t.element.querySelector(".selection-actions"),count:t.element.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.syncSortableSelection(e,o.selectedItems),this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(e,o),o}addGroupSelectionHandler(e,t){const o=`${e}_${t}`;if(this.selectionHandlers.has(o))return this.selectionHandlers.get(o);const s=this.groupElements.get(t);if(!s?.element)return;const a=new window.jvbHandleSelection({container:s.element,ui:{selectAll:s.element.querySelector(this.selectors.groups.selectAll),bulkControls:s.element.querySelector(this.selectors.groups.actions),count:s.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return a.subscribe(((t,o)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,o.selectedItems);break;case"select-all":this.handleSelectAll(o.container,o.selected)}})),this.selectionHandlers.set(o,a),a}handleSelectAll(e,t){}getFieldData(e){const t=this.fieldStore.get(e);return t?(Array.isArray(t.uploads)?t.uploads=new Set(t.uploads):t.uploads||(t.uploads=new Set),Array.isArray(t.groups)||(t.groups=[]),t):null}async saveFieldData(e){await this.fieldStore.save({...e,timestamp:Date.now()})}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const o={field:{selector:this.selectors.field.field,key:"uploader",getRuntimeData:e=>this.fieldElements.get(e),getStoreData:e=>this.getFieldData(e)},upload:{selector:this.selectors.items.item,key:"uploadId",getRuntimeData:e=>this.uploadElements.get(e),getStoreData:e=>this.uploadStore.get(e)},group:{selector:this.selectors.groups.container,key:"groupId",getRuntimeData:e=>this.groupElements.get(e),getStoreData:e=>{const t=this.groupElements.get(e);if(!t)return null;const o=this.getFieldData(t.fieldId);return o?.groups?.find((t=>t.id===e))}}},s=o[t];if(!s)return null;const a=e.closest(s.selector);if(!a)return null;const r=a.dataset[s.key];return{...s.getRuntimeData(r),...s.getStoreData(r)}}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){const t=this.getFromElement(e,"field");return t?.id??null}getUploadIdFromElement(e){const t=this.getFromElement(e,"upload");return t?.id??null}getGroupIdFromElement(e){const t=this.getFromElement(e,"group");return t?.id??null}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]||0}createUploadElement(e,t=!1){let o=window.getTemplate("uploadItem");if(!o)return;o.dataset.uploadId=e.id,o.dataset.subtype=e.subtype||"image";let[s,a,r,i,n]=[o.querySelector('[name="featured"]'),o.querySelector("img"),o.querySelector("video"),o.querySelector("label > span"),o.querySelector("details")];switch(s&&(s.value=e.id),e.subtype){case"image":a&&(a.src=e.preview,a.alt=e.meta?.originalName||""),r?.remove(),i?.remove();break;case"video":r&&(r.src=e.preview),a?.remove(),i?.remove();break;case"document":const t=e.meta?.originalName||"",o=t.split(".").pop()?.toLowerCase()||"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},n=window.getIcon(s[o]||"file");i&&(i.innerText=t,i.prepend(n)),a?.remove(),r?.remove()}if(n){let e=window.getTemplate("uploadMeta");e&&n.append(e)}return o.draggable=t,o.querySelectorAll("input").forEach((t=>{let o=t.id;if(o){let s=o+e.id,a=t.parentNode.querySelector(`label[for="${o}"]`);t.id=s,a&&(a.htmlFor=s)}})),o}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),250)}async persistFieldState(e){const t=this.getFieldData(e);t&&await this.saveFieldData(t)}async saveBlobData(e,t){const o=await t.arrayBuffer(),s=this.uploadStore.get(e)||{id:e};s.blobData={buffer:o,name:t.name,type:t.type,size:t.size,lastModified:t.lastModified||Date.now()},await this.uploadStore.save(s)}async getBlobData(e){const t=this.uploadStore.get(e);if(!t?.blobData)return null;const o=new Blob([t.blobData.buffer],{type:t.blobData.type});return new File([o],t.blobData.name,{type:t.blobData.type,lastModified:t.blobData.lastModified})}async getFilesForForm(e){const t=e.querySelectorAll("[data-upload-field]"),o=[];for(const e of t){const t=this.determineFieldId(e),s=await this.getFilesForField(t);o.push(...s)}return o}async getFilesForField(e){const t=this.getFieldData(e);if(!t?.uploads)return[];const o=[],s=t.uploads instanceof Set?Array.from(t.uploads):t.uploads;for(const e of s){const s=this.uploadStore.get(e);if(!s)continue;const a=await this.getBlobData(e);a&&o.push({file:a,uploadId:e,fieldName:t.config.name,meta:s.meta||{}})}return o}handleFieldStoreEvent(e,t){if("data-loaded"===e)this.fieldStoreReady=!0,this.checkIfBothStoresReady()}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.uploadStoreReady=!0,this.checkIfBothStoresReady();break;case"item-saved":this.showSaveIndicator(t.key)}}checkIfBothStoresReady(){this.fieldStoreReady&&this.uploadStoreReady&&!this.hasCheckedForUploads&&(this.hasCheckedForUploads=!0,this.checkForStoredUploads())}async checkForStoredUploads(){const e=this.fieldStore.getAll().filter((e=>{if(!e.uploads)return!1;return(e.uploads instanceof Set?Array.from(e.uploads):Array.isArray(e.uploads)?e.uploads:[]).some((e=>{const t=this.uploadStore.get(e);return t&&!t.operationId&&["completed","processed","local_processing","processed-original"].includes(t.status)}))}));0!==e.length&&await this.showRecoveryNotification(e)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),o=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let s,a=window.getTemplate("restoreNotification");if(!a)return void console.error("Restore notification template not found");if(o>0){s=`${o} ${o>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else s=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const r=a.querySelector(".restore-details");r&&(r.textContent=s);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const o=e.querySelector("h3");o&&(o.textContent=t.config.name||"Unnamed Field");const s=e.querySelector(".item-grid.restore");for(let e of t.uploads){const o=this.uploadStore.get(e);let a=window.getTemplate("uploadItem");if(!a)continue;const r=await this.getBlobData(o.id);if(r)try{const e=this.createPreviewUrl(r);let[s,i,n,l,d]=[a.querySelector('[name="featured"]'),a.querySelector("img"),a.querySelector("video"),a.querySelector("label > span"),a.querySelector("details")];a.dataset.uploadId=o.id,a.dataset.fieldId=t.id;let c=this.getSubtypeFromMime(r.type);switch(a.dataset.subtype=c,c){case"image":[i.src,i.alt]=[e,r.name??o.meta?.originalName??""],n.remove(),l.remove();break;case"video":n.src=e,i.remove(),l.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}l.innerText=o.originalFile.name,l.prepend(t),i.remove(),n.remove()}a.dataset.previewUrl=e}catch(e){console.warn("Failed to create preview for upload:",o.id,e)}const i=a.querySelector("summary span");i&&(i.textContent=o.meta?.originalName||"Unknown file");const n=a.querySelector("details");n&&o.meta&&(n.textContent=`${this.formatBytes(o.meta.size)} • ${o.meta.type}`),a.querySelectorAll("input").forEach((e=>{let t=e.id;if(t){let s=t+o.id,a=e.parentNode.querySelector(`label[for="${t}"]`);e.id=s,a&&(a.htmlFor=s)}})),s&&s.appendChild(a)}a.querySelector(".wrap").appendChild(s)}document.querySelector(".field.upload").appendChild(a),a=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(a),this.restoreSelection=new window.jvbHandleSelection({container:a,ui:{selectAll:a.querySelector("#select-all-restore"),count:a.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}async handleRestoreAll(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=[];e.querySelectorAll(".item.upload").forEach((e=>{let o=e.dataset.uploadId,s=e.dataset.fieldId;t.push({uploadId:o,fieldId:s})})),await this.restoreSelectedUploads(t),this.cleanupRestore()}showSaveIndicator(e){}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){await this.fieldStore.clear(),await this.uploadStore.clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((o=>{try{o(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.uploadElements.clear(),this.fieldElements.clear(),this.groupElements.clear(),this.selected.clear(),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.queue=window.jvbQueue,this.error=window.jvbError,this.subscribers=new Set,this.initStores(),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.sortables=new Map,this.previewUrls=new Set,this.initElements(),this.initListeners()}initStores(){const{uploads:e,groups:t}=window.jvbStore.register("uploads",[{storeName:"uploads",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"status",keyPath:"status"},{name:"group",keyPath:"group"},{name:"src",keyPath:"src"}]},{storeName:"groups",keyPath:"id",indexes:[{name:"field",keyPath:"field"},{name:"src",keyPath:"src"}]}]);this.stores={uploads:e,groups:t,ready:[]},this.stores.uploads.subscribe(this.handleStores.bind(this,"uploads")),this.stores.groups.subscribe(this.handleStores.bind(this,"groups")),this.queue.subscribe(((e,t)=>{if(!["uploads","uploads/meta","uploads/groups"].includes(t.endpoint))return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data?.fieldId;if(s)switch(e){case"cancel-operation":this.handleOperationCancelled(s).then((()=>{}));break;case"operation-status":this.handleFieldStatus(s,t).then((()=>{}));break;case"operation-completed":this.handleOperationComplete(t,s).then((()=>{}));break;case"operation-failed":case"operation-failed-permanent":this.handleOperationFailed(t,s).then((()=>{}))}}))}storesReady(){return 2===this.stores.ready.length}handleStores(e,t){"data-ready"===t&&(this.stores.ready.push(e),this.storesReady()&&this.checkRecovery())}initWorker(){this.worker=null,this.workerState={worker:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:3e3,maxConcurrent:3,restartAfterTimeout:!0}}}initElements(){this.selectors={fields:{field:"[data-upload-field]",input:'input[type="file"]',dropZone:".file-upload-container",preview:".preview-wrap",grid:".item-grid.preview",progress:{progress:".file-upload-container .progress",fill:".file-upload-container .progress .fill",details:".file-upload-container .progress .details",icon:".file-upload-container .progress .icon"},selectAll:'[name="select-all-uploads"]',actions:".selection-actions",count:".selection-count",hidden:'input[type="hidden"]'},groups:{container:".group-display",grid:".item-grid.groups",empty:".empty-group",header:".sidebar .header"},group:{item:".upload-group",actions:".selection-actions",selectAll:'[name="select-all-group"]',count:".group-header .info",fields:"details .fields",grid:".item-grid.group",total:".group-content .group-count"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',image:"img",details:"details",progress:{progress:".progress",fill:".fill",details:".details",icon:".icon"}}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.dragEnterHandler=this.handleDragEnter.bind(this),this.dragLeaveHandler=this.handleDragLeave.bind(this),this.dragOverHandler=this.handleDragOver.bind(this),this.dropHandler=this.handleDrop.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}async setUpload(e,t){const s={...{id:e,attachment:null,group:null,field:null,src:window.location.href,blob:null,status:"local_processing",operationId:null,fields:{}},...t};return Object.preventExtensions(s),await this.stores.uploads.save(s),s}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls.delete(e))}formatFile(e){return e.blob?new File([e.blob],e.fields.originalName||"file",{type:e.fields.type||e.blob.type,lastModified:e.fields.lastModified||Date.now()}):null}handleClick(e){let t=window.targetCheck(e,this.selectors.fields.dropZone);t&&!e.target.matches("input, button, a")&&t.querySelector(this.selectors.fields.input)?.click();const s=window.targetCheck(e,"[data-action]");s&&this.handleAction(s)}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(s).then((()=>{}));break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e).then((()=>{}));break;case"upload":this.queueUploads("uploads/groups",s).then((()=>{}));break;case"restore":this.handleRestoreSelected().then((()=>{}));break;case"restore-all":this.handleRestoreAll().then((()=>{}));break;case"clear-cache":this.handleClearCache().then((()=>{}))}}handleChange(e){let t=this.getFieldIdFromElement(e.target);if(!t)return;if(e.target.matches(this.selectors.fields.input)){const s=Array.from(e.target.files);return void(s.length>0&&this.processFiles(t,s).then((()=>{})))}if(e.target.matches(this.selectors.items.checkbox)||e.target.matches(this.selectors.items.featured)||e.target.matches('[name*="select-"]'))return;let s=this.fields.get(t);s&&s.config.autoUpload&&("post_group"===s.config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e).then((()=>{})))}handleGroupMetaChange(e){const t=e.closest(this.selectors.group.fields);if(!t)return;const s=t.dataset.groupId,r=this.stores.groups.get(s);r&&window.debouncer.schedule(`group-meta-${s}`,(async(e,t)=>{let s=e.name.replace(`${t}_`,"").replace(`${t}[`,"").replace("]","");r.fields[s]=e.value,await this.setGroup(t,r)}),300)}handleDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.fields.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleDragLeave(e){const t=e.target.closest(this.selectors.fields.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.fields.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleDrop(e){const t=e.target.closest(this.selectors.fields.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover"),t.classList.add("uploading");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r&&(this.processFiles(r,s).then((()=>{})),this.a11y.announce(`${s.length} file(s) dropped for upload`))}async queueUploads(e,t){let s=new FormData;const r=this.fields.get(t);if(!r)return;let i=this.stores.uploads.filterByIndex({field:t});if(0===i.length)return;const[o,a]=["uploads"===e,"uploads/groups"===e];let l,n,d,u,c;s.append("fieldId",r.id),s.append("content",r.config.content),o&&(s.append("mode",r.config.mode),s.append("field_name",r.config.name),s.append("fieldId",r.id),s.append("field_type",r.config.type),s.append("subtype",r.config.subtype),s.append("item_id",r.config.itemID),s.append("destination",r.config.destination)),a?({posts:l,uploadMap:n,files:d}=this.collectGroups(t)):o&&({uploadMap:n,files:d}=this.collectUploads(t)),a&&s.append("posts",JSON.stringify(l)),d.forEach((e=>{s.append("files[]",e)})),s.append("upload_ids",JSON.stringify(n)),o?(u=`Uploading ${i.length} file${i.length>1?"s":""} to server...`,c=`Uploading ${i.length} file${i.length>1?"s":""}...`):a&&(u=`Creating ${l.length} ${r.config.content}${l.length>1?"s":""} from uploads...`,c=`Creating ${l.length} post${l.length>1?"s":""}...`),await this.setBulkUpload(i,"status","queued");let p=this.sendToQueue(e,s,u,c);if("uploads/groups"===e){let e=r.element.closest("details");e&&(e.open=!1)}return p?(r.operationId=p,await this.setBulkUpload(i,"operationId",p),await this.setBulkUpload(i,"status","uploading"),await this.setBulkGroup(t,"operationId",p),this.fields.set(r.id,r)):await this.setBulkUpload(i,"status","failed"),this.notify("sent-to-queue",t),p}async sendToQueue(e,t,s="",r="",i=!1){""===r&&(r=s);const o={endpoint:e,method:"POST",data:t,title:s,popup:r,canMerge:i,sendNow:"uploads/groups"===e,headers:{action_nonce:window.auth.getNonce("dash")},append:"_upload"};try{return await this.queue.addToQueue(o)}catch(e){return this.error.log(e,{component:"UploadManager",action:"sentToQueue"}),!1}}collectGroups(e){let t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e}),r=[],i=[],o=[];for(const e of s){const s={images:[],fields:e.fields??{}},a=t.filter((t=>t.group===e.id));for(const e of a){const t=this.formatFile(e);if(t){o.push(t);const r={upload_id:e.id,index:i.length};let a=this.uploads.get(e.id);a.ui?.featured?.checked&&(s.fields.featured=e.id),s.images.push(r),i.push(e.id)}}r.push(s)}const a=t.filter((e=>!e.group));for(const e of a){const t={images:[],fields:{}},s=this.formatFile(e);if(s){o.push(s);const r={upload_id:e.id,index:i.length};t.images.push(r),i.push(e.id)}r.push(t)}return{posts:r,uploadMap:i,files:o}}collectUploads(e){let t=this.stores.uploads.filterByIndex({field:e});if(0===t.length)return;let s=[],r=[];for(const e of t){const t=this.formatFile(e);t&&(r.push(t),s.push(e.id))}return{uploadMap:s,files:r}}async queueUploadMeta(e){const t=e.target.closest(this.selectors.items.item)?.dataset.uploadId,s=this.stores.uploads.get(t);if(!t||!s)return;if(!this.fields.get(s.field))return;let r={};r[e.target.name]=e.target.value,s.fields={...s.fields,...r},await this.setUpload(s.id,s);let i={};return i[s.attachmentId??s.id]=s.fields,await this.sendToQueue("uploads/meta",i,"Uploading Meta","",!0)}async handleOperationComplete(e,t){const s=e.response;if(s?.data){const e=Array.isArray(s.data)?s.data:Object.values(s.data);for(const t of e)if(t.upload_id&&t.attachment_id){const e=this.stores.uploads.get(t.upload_id);e&&(e.attachmentId=t.attachment_id,e.status="completed",await this.stores.uploads.save(e))}}const r=this.stores.uploads.filterByIndex({field:t}),i=this.stores.groups.filterByIndex({field:t});await Promise.all([...r.filter((e=>"completed"===e.status)).map((e=>this.clearUpload(e.id))),...i.map((e=>this.stores.groups.delete(e.id)))]),this.notify("uploads-complete",{fieldId:t,response:s})}scanFields(e,t=!0){e.querySelectorAll(this.selectors.fields.field).forEach((e=>this.registerField(e,t)))}registerField(e,t=!0,s=null){const r={element:e,id:s||this.determineFieldId(e),config:this.extractFieldConfig(e,t),uploads:new Set,operationId:null,groups:[],ui:window.uiFromSelectors(this.selectors.fields,e),groupUI:window.uiFromSelectors(this.selectors.groups,e)};return this.fields.set(r.id,r),e.dataset.uploader=r.id,this.getSelectionHandler(r.id),"single"!==r.config.type&&this.initSortable(r.id),r.id}extractFieldConfig(e,t){return{autoUpload:t,destination:e.dataset.destination||"meta",content:this.extractFieldContent(e),mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:this.extractFieldItemId(e)??0,maxFiles:parseInt(e.dataset.maxFiles)??25,subType:e.dataset.subtype??"image"}}extractFieldContent(e){return e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||null}extractFieldItemId(e){return e.dataset.itemId||e.closest("dialog")?.dataset.itemId||null}determineFieldId(e){let t=this.extractFieldContent(e);t=null===t?"":t+"_";let s=this.extractFieldItemId(e);s=null===s?"":s+"_";return`${t}${s}${e.dataset.field||""}`}getFieldIdFromElement(e){const t=e.closest(this.selectors.fields.field);return t?.dataset.uploader||null}updateFieldProgress(e,t,s,r){const i=this.fields.get(e);i&&window.showProgress(i.ui.progress,t,s,r)}getWorker(){return this.workerState.worker||"undefined"==typeof OffscreenCanvas||(this.workerState.worker=new Worker("worker.js"),this.workerState.worker.onmessage=e=>this.handleWorkerMessage(e),this.workerState.worker.onerror=e=>this.handleWorkerError(e)),this.workerState.worker}handleWorkerMessage(e){const{id:t,blob:s}=e.data,r=this.workerState.tasks.get(t);r&&(clearTimeout(r.timeoutId),r.resolve(s),this.workerState.tasks.delete(t))}handleWorkerError(e){this.workerState.tasks.forEach((t=>{clearTimeout(t.timeoutId),t.reject(e)})),this.workerState.tasks.clear(),this.restartWorker()}restartWorker(){this.workerState.worker&&(this.workerState.worker.terminate(),this.workerState.worker=null),this.workerState.restart.count++}async processImages(e,t=2200,s=2200){const r=[],i=[...e],o=this.workerState.settings.maxConcurrent,a=async()=>{for(;i.length>0;){const e=i.shift();r.push(await this.processImage(e,t,s))}};return await Promise.all(Array.from({length:Math.min(o,e.length)},(()=>a()))),r}async processImage(e,t=2200,s=2200,r=3e3){if("undefined"==typeof OffscreenCanvas)return this.resizeImage(e,t,s);try{return await this.withTimeout(this.workerImage(e,t,s),r)}catch(r){return this.resizeImage(e,t,s)}}withTimeout(e,t){return Promise.race([e,new Promise(((e,s)=>setTimeout((()=>s(new Error("Timeout"))),t)))])}async workerImage(e,t=2200,s=2200){const{settings:r,restart:i}=this.workerState;if(i.count>=i.max)throw new Error("Worker max restarts exceeded");const o=await createImageBitmap(e);let{width:a,height:l}=o;if(a>t||l>s){const e=Math.min(t/a,s/l);a=Math.round(a*e),l=Math.round(l*e)}const n=this.getWorker(),d=crypto.randomUUID();return new Promise(((t,s)=>{const i=setTimeout((()=>{this.workerState.tasks.delete(d),r.restartAfterTimeout&&this.restartWorker(),s(new Error("Timeout"))}),r.timeout);this.workerState.tasks.set(d,{resolve:t,reject:s,timeoutId:i}),n.postMessage({id:d,imageBitmap:o,width:a,height:l,type:e.type,quality:.9},[o])}))}resizeImage(e,t,s){return new Promise((r=>{const i=new Image;i.onload=()=>{URL.revokeObjectURL(i.src);let{width:o,height:a}=i;if(o>t||a>s){const e=Math.min(t/o,s/a);o=Math.round(o*e),a=Math.round(a*e)}const l=document.createElement("canvas");l.width=o,l.height=a,l.getContext("2d").drawImage(i,0,0,o,a),l.toBlob(r,e.type,.9)},i.src=URL.createObjectURL(e)}))}async processFiles(e,t){let s=this.fields.get(e);if(!s)return;s.groupUI.container&&(s.groupUI.container.hidden=!1);const r=t.length;let i=0;this.updateFieldProgress(e,0,r,"Processing files...");const o=await Promise.all(t.map((async t=>{const s=window.generateID("upload"),r=await this.setUpload(s,{id:s,field:e,status:"local_processing",blob:null,fields:{originalName:t.name,originalSize:t.size,type:t.type,lastModified:t.lastModified}}),i=await this.createUpload(s,t,e);return this.uploads.set(s,{element:i,ui:window.uiFromSelectors(this.selectors.items,i)}),await this.addToGroup(s,null),{uploadId:s,upload:r,file:t}}))),a=o.filter((e=>e.file.type.startsWith("image/"))),l=o.filter((e=>!e.file.type.startsWith("image/"))),n=await this.processImages(a.map((e=>e.file)));for(let t=0;t<a.length;t++){const{uploadId:s,upload:o}=a[t];o.blob=n[t],o.fields.size=n[t].size,o.status="queued",await this.setUpload(s,o),i++,this.updateFieldProgress(e,i,r,"Processing files...")}for(const{uploadId:t,upload:s,file:o}of l)s.blob=o,s.status="queued",await this.setUpload(t,s),i++,this.updateFieldProgress(e,i,r,"Processing files...");this.maybeLockUploads(e),s.config.autoUpload&&"post_group"!==s.config.destination&&await this.queueUploads("uploads",e)}async checkRecovery(){const e=this.stores.uploads.filterByIndex({status:["local_processing","queued","uploading"]});if(0===e.length)return;let t=window.getTemplate("restoreNotification");if(!t)return void this.error.log("No restore notification",{component:"UploadManager",src:window.location.href});const s=new Map;e.forEach((e=>{const t=e.src||"unknown";s.has(t)||s.set(t,[]),s.get(t).push(e)}));const r=window.location.href;let i=s.size>1?` across ${s.size} pages`:"",o=e.length>1?"uploads":"upload",a=`${e.length} ${o} can be recovered${i}`,l=t.querySelector(".details");l&&(l.textContent=a);let n=1;for(const[e,i]of s){let s=window.getTemplate("restoreField");if(!s)continue;let o=this.registerField(s,!1,"recovery_"+n),a=this.fields.get(o);n++;let l=e===r,[d,u,c]=[s.querySelector("h3"),s.querySelector("h3 a"),s.querySelector(".item-grid")];s.open=l,l?(u.remove(),d.textContent="From this page:"):[u.href,u.title,u.textContent]=[e,"Navigate to Page and Restore",e];let p=[...new Set(i.map((e=>e.group??"preview")))];for(let e of p){let t="preview"===e||this.stores.groups.get(e);if(!t)continue;let s=await this.createGroupElement(e,a.id),r=s.querySelector(".item-grid"),o=i.filter((t=>t.group===("preview"===e)?null:e));for(const[e,r]of Object.entries(t.fields??{})){let t=s.querySelector(`input[name*="${e}"]`);t&&(t.value=r)}for(let e of o){let t=await this.createUpload(e.id,this.formatFile(e),a.id);r.append(t)}c.append(s)}t.querySelector(".wrap").append(s)}document.body.append(t),t=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(t),this.restoreSelection=new window.jvbHandleSelection({container:t,wrapper:".restore-uploads .wrap",bulkControls:".selection-actions",selectAll:"#select-all-restore",count:".selection-count"}),this.restoreModal.handleOpen()}async handleRestoreSelected(){if(!this.restoreSelection)return;let e=Array.from(this.restoreSelection.selectedItems);0!==e.length&&await this.restoreSelectedUploads(e)}async handleRestoreAll(){if(!this.restoreModal)return;const e=Array.from(this.restoreModal.modal.querySelectorAll(".item.upload")).map((e=>e.dataset.uploadId));await this.restoreSelectedUploads(e)}async restoreSelectedUploads(e){let t=window.location.href,s=Array.from(this.stores.uploads.data.values()).filter((s=>e.includes(s.id)&&s.src===t)),r=[...new Set(s.map((e=>e.group)))].filter(Boolean),i=s[0].field;if(!document.querySelector(`[data-uploader="${i}"]`))return void console.log("No field found for "+i);let o=this.fields.get(i);o.groupUI.container&&(o.groupUI.container.hidden=!1);let a=[];for(let e of r){let t=this.stores.groups.get(e);await this.createGroup(i,e);let r=this.groups.get(e),o=s.filter((t=>t.group===e));if(t&&this.groups.has(e)){let e=t.fields;for(const[t,s]of Object.entries(e)){let e=r.element.querySelector(`input[name*="${t}"]`);e&&(e.value=s)}}else e=null;for(let t of o){let s=await this.createUpload(t.id,this.formatFile(t),i);this.uploads.set(t.id,{element:s,ui:window.uiFromSelectors(this.selectors.items,s)}),await this.addToGroup(t.id,e),a.push(t.id)}}let l=s.filter((e=>!a.includes(e.id)));for(let e of l){let t=await this.createUpload(e.id,this.formatFile(e),i);this.uploads.set(e.id,{element:t,ui:window.uiFromSelectors(this.selectors.items,t)}),await this.addToGroup(e.id,null)}this.cleanupRestore()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}getStatusText(e){return{received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"}[e]||e}getStatusProgress(e){return{local_processing:28,queued:50,uploading:66,pending:75,processing:89,completed:100}[e]??0}async createUpload(e,t,s){let r=window.getTemplate("uploadItem");if(!r)return null;let i=this.fields.get(s);if(!i)return null;r.dataset.uploadId=e;let o=this.getSubtypeFromMime(t.type)||"image";r.dataset.subtype=o;let[a,l,n,d,u]=[r.querySelector('[name="featured"]'),r.querySelector("img"),r.querySelector("video"),r.querySelector("label > span"),r.querySelector("details")];switch(a&&(a.value=e),o){case"image":if(l){const e=this.createPreviewUrl(t);l.src=e,l.alt=t.name||"",l.dataset.previewUrl=e}n?.remove(),d?.remove();break;case"video":if(n){const e=this.createPreviewUrl(t);n.src=e,n.dataset.previewUrl=e}l?.remove(),d?.remove();break;case"document":let e=t.name.split(".").pop()?.toLowerCase()??"",s={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},r=window.getIcon(s[e]??"file");d&&(d.innerText=t.name,d.prepend(r)),l?.remove(),n?.remove()}if(u){let e=window.getTemplate("uploadMeta");e&&u.append(e)}return r.draggable="single"!==i.config.type??!1,r.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e,i=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,i&&(i.htmlFor=r)}})),r}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}async handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId;confirm("Remove this item?")&&(await this.removeUpload(s),this.a11y.announce("Item removed"))}async setBulkUpload(e,t,s){const r=Array.from(e).map((async e=>("status"===t&&await this.setUploadStatus(e,s),e[t]=s,this.stores.uploads.save(e))));await Promise.all(r)}async setUploadStatus(e,t){e.progress&&window.showProgress(e.progress,this.getStatusProgress(t),100,this.getStatusText(t),this.queue.icons[t]??"")}async removeUpload(e){let t=this.stores.uploads.get(e);if(!t)return;if(t.group){let s=this.stores.groups.get(t.group);s.uploads=s.uploads.filter((t=>t!==e)),0===s.uploads.length&&await this.removeGroup(s.id,!1)}await this.clearUpload(e),this.maybeLockUploads(t.field);let s=this.selectionHandlers.get(t.field);s&&s.deselect(e),this.a11y.announce("Upload removed")}async clearUpload(e){const t=this.uploads.get(e);if(t&&(this.revokePreviewUrl(t.preview),t.element)){const e=t.element.dataset.previewUrl;this.revokePreviewUrl(e),t.element.remove()}this.uploads.delete(e),await this.stores.uploads.delete(e)}async handleAddToGroup(e){const t=this.selected.get(e);if(!t||0===t.size)return;let s=await this.createGroup(e);s&&(await Promise.all(Array.from(t).map((e=>this.addToGroup(e,s)))),this.selectionHandlers.get(e)?.clearSelection(),this.a11y.announce(`Created group with ${t.size} items`))}async createGroup(e,t=null){let s=this.fields.get(e);if(!s)return;t||(t=window.generateID("group"));const r=this.createGroupElement(t,e);if(!r)return null;s.groupUI.grid.append(r);const i=r.querySelector(".item-grid");i&&(i.dataset.groupId=t,this.createSortableForGrid(e,i,t));let o=this.stores.groups.data.has(t)?this.stores.groups.data.get(t):{};return await this.setGroup(t,{...o,id:t,field:e}),t}createGroupElement(e,t=null){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,t&&(s.dataset.fieldId=t);const r=s.querySelector("[data-select-all]");if(r){const t=`select-all-${e}`,i=s.querySelector(`label[for="${r.id}"]`);r.id=t,r.name=t,i&&(i.htmlFor=t)}let i=window.getTemplate("groupMetadata"),o=s.querySelector(".fields");if(i&&o){o.append(i);let t=o.querySelector('[name="post_title"]'),s=o.querySelector('[name="post_excerpt"]');t&&(t.id=`${e}_title`,t.name=`${e}[post_title]`),s&&(s.id=`${e}_excerpt`,s.name=`${e}[post_excerpt]`)}else s.querySelector("details")?.remove();const a=s.querySelector(".item-grid");return a&&(a.dataset.groupId=e),this.groups.set(e,{element:s,ui:window.uiFromSelectors(this.selectors.group,s)}),s}async setGroup(e,t){const s={...{id:e,src:window.location.href,uploads:[],operationId:null,field:null,fields:{}},...t};Object.preventExtensions(s),await this.stores.groups.save(s)}async setBulkGroup(e,t,s){let r=this.stores.groups.filterByIndex({field:e});if(0===r.length)return;let i=r.map((e=>{e[t]=s,this.stores.groups.save(e)}));await Promise.all(i)}async addToGroup(e,t=null){const s=this.stores.uploads.get(e),r=this.uploads.get(e);if(!s||!r)return;const i=this.fields.get(s.field);if(!i)return;if(null!==r.element?.parentElement&&(!t&&null===s.group||t===s.group))return void this.handleReorder(s.field,t);if(s.group){const t=this.stores.groups.get(s.group);t&&(t.uploads=t.uploads.filter((t=>t!==e)),0===t.uploads.length&&await this.removeGroup(t.id,!1))}if(r.ui.checkbox&&(r.ui.checkbox.checked=!1),this.selected.get(s.field)?.has(e)&&this.selected.get(s.field).delete(e),r.ui.featured&&(r.ui.featured.hidden=!t),t){r.ui.featured&&(r.ui.featured.name=`${t}_featured`);let i=this.stores.groups.get(t);i&&(i.uploads.push(e),s.group=t,this.stores.groups.save(i))}else s.group=null;let o=t?this.groups.get(t)?.ui.grid:i.ui.grid;o&&o.append(r.element),this.stores.uploads.save(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.group.item);if(!t)return;let s=t.dataset.groupId;if(!confirm("Delete this group? Items will be moved back to the upload area."))return;let r=this.stores.uploads.filterByIndex({group:s});Promise.all(r.map((e=>this.addToGroup(e.id,null)))).then((()=>{this.removeGroup(s,!1).then((()=>{})),this.a11y.announce("Group deleted. Items returned to upload area")}))}async removeGroup(e,t=!0){let s=this.groups.get(e),r=this.stores.groups.get(e);if(!r)return;let i=!0;t&&r.uploads.length>0&&(i=window.confirm("Keep uploads in this group?")),await Promise.all(r.uploads.map((e=>i?this.addToGroup(e,null):this.removeUpload(e))));const o=this.getGroupKey(r.field,e),a=this.sortables.get(o);a?.destroy&&a.destroy(),this.sortables.delete(o),s?.element&&s.element.remove(),this.groups.delete(e),await this.stores.groups.delete(e),this.a11y.announce("Group removed")}maybeLockUploads(e){let t=this.fields.get(e);if(!t||!t.ui.dropZone)return;let s=this.stores.uploads.filterByIndex({field:e}).length,r=t.config.maxFiles??25;t.ui.dropZone.hidden=s>=r}async handleOperationCancelled(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all([...t.map((e=>this.removeUpload(e.id))),...s.map((e=>this.removeGroup(e.id,!1)))]),this.a11y.announce("Upload Cancelled")}async handleOperationFailed(e,t){await this.setBulkUpload(this.stores.uploads.filterByIndex({field:t}),"status","failed")}async handleFieldStatus(e,t){let s=t.status,r=this.stores.uploads.filterByIndex({field:e});await this.setBulkUpload(r,"status",s)}getGroupKey(e,t=null){return t?`${e}_${t}`:`${e}`}getSelectionHandler(e){let t=this.getGroupKey(e);if(!this.selectionHandlers.has(t)){let s=this.fields.get(e);if(!s)return;let r=new window.jvbHandleSelection({container:s.element,item:this.selectors.items.item,count:this.selectors.fields.count,bulkControls:this.selectors.fields.actions,checkbox:this.selectors.items.checkbox,selectAll:this.selectors.fields.selectAll,wrapper:`${this.selectors.fields.preview}, ${this.selectors.group.item}`});r.subscribe(((t,s)=>{this.selected.set(e,s.selectedItems),console.log(Array.from(this.selected)),this.syncSortableSelection(e,s.selectedItems)})),this.selectionHandlers.set(t,r)}return this.selectionHandlers.get(t)}initSortable(e){if(!window.Sortable)return;const t=this.fields.get(e);t&&(!Sortable._multiDragMounted&&Sortable.MultiDrag&&(Sortable.mount(new Sortable.MultiDrag),Sortable._multiDragMounted=!0),this.createSortable(e,t.ui.grid,null),this.initEmptyGroupDropZone(e))}createSortable(e,t,s){if(!t)return null;const r=this.getGroupKey(e,s);if(this.sortables.has(r))return this.sortables.get(r);const i=new Sortable(t,{animation:150,draggable:".item",multiDrag:!0,selectedClass:"selected",avoidImplicitDeselect:!0,group:{name:e,pull:!0,put:!0},ghostClass:"ghost",chosenClass:"chosen",dragClass:"dragging",onStart:()=>this.syncSortableSelection(e),onEnd:t=>this.sortableDrop(t,e)});return this.sortables.set(r,i),i}initEmptyGroupDropZone(e){const t=this.fields.get(e),s=t?.groupUI?.empty;s&&(s.addEventListener("dragover",(e=>{e.preventDefault(),e.dataTransfer.dropEffect="move",s.classList.add("drag-over")})),s.addEventListener("dragleave",(e=>{s.contains(e.relatedTarget)||s.classList.remove("drag-over")})),s.addEventListener("drop",(async t=>{t.preventDefault(),s.classList.remove("drag-over");const r=this.selected.get(e);if(!r||0===r.size)return;const i=await this.createGroup(e);i&&(await Promise.all(Array.from(r).map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(e)?.clearSelection())})))}async sortableDrop(e,t){const s=e.to,r=(e.items?.length>0?Array.from(e.items):[e.item]).map((e=>e.dataset.uploadId)).filter(Boolean);if(0===r.length)return;const i=s.dataset.groupId||null;await Promise.all(r.map((e=>this.addToGroup(e,i)))),this.selectionHandlers.get(t)?.clearSelection()}syncSortableSelection(e){const t=this.selected.get(e)||new Set;for(const[s,r]of this.uploads){const i=this.stores.uploads.get(s);if(!i||i.field!==e)continue;const o=r.element;if(!o)continue;const a=t.has(s);a&&!o.classList.contains("selected")?Sortable.utils.select(o):!a&&o.classList.contains("selected")&&Sortable.utils.deselect(o)}}handleReorder(e,t=null){let s=t?this.groups.get(t)?.ui.grid:this.fields.get(e)?.ui.grid;if(!s)return void console.log("Couldn't Reorder items...");let r=Array.from(s.querySelectorAll(this.selectors.items.item+":not(.ghost)")).map((e=>e.dataset.uploadId)).filter((e=>e));if(t){let e=this.groups.get(t);e&&(e.uploads=r)}else{let t=this.fields.get(e)?.ui.hidden;t&&(t.value=r.join(","))}this.a11y.announce("Items reordered")}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}destroy(){this.subscribers.clear(),this.previewUrls.forEach((e=>{this.revokePreviewUrl(e)})),this.previewUrls.clear()}cleanupAllPreviewUrls(){this.previewUrls.forEach((e=>this.revokePreviewUrl(e))),this.previewUrls.clear()}async handleClearCache(){const e=window.location.href,t=this.stores.uploads.filterByIndex({src:e}),s=this.stores.groups.filterByIndex({src:e});await Promise.all([...t.map((e=>this.clearUpload(e.id))),...s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id))))]),this.a11y.announce("Cache cleared for this page")}async getFilesForForm(e){const t=e.querySelectorAll(this.selectors.fields.field),s=[];for(const e of t){const t=this.determineFieldId(e),r=e.dataset.field,i=this.stores.uploads.filterByIndex({field:t});for(const e of i){const t=this.formatFile(e);t&&s.push({file:t,fieldName:r,uploadId:e.id,meta:e.fields||{}})}}return s}async clearFieldFromStores(e){const t=this.stores.uploads.filterByIndex({field:e}),s=this.stores.groups.filterByIndex({field:e});await Promise.all(t.map((e=>this.clearUpload(e.id)))),await Promise.all(s.map((e=>(this.groups.get(e.id)?.element?.remove(),this.groups.delete(e.id),this.stores.groups.delete(e.id)))))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbUploads=new e)}))}))})();
\ No newline at end of file
diff --git a/assets/js/min/utility.min.js b/assets/js/min/utility.min.js
index 1f5ffb6..dc0f567 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=e-new Date,i=n<0,o=Math.floor(Math.abs(n)/1e3),r=Math.floor(o/60),s=Math.floor(r/60),a=Math.floor(s/24);if(0===r)return"Just now";let c="";if(o<10)c="a moment";else if(o<60)c="less than a minute";else if(r<5)c="a few minutes";else if(s<24)c=0===s?`${r} ${1===r?"minute":"minutes"}`:`${s} ${1===s?"hour":"hours"}`;else{if(!(a<7))return e.toLocaleDateString();c=`${a} ${1===a?"day":"days"}`}return i?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let s=!0;return async function(){for(;s;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){s=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function s(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function a(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function c(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),a(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(c))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{s(),a(window.scrollY||t.scrollTop||0)}),20)})),s(),a(n)})();
\ No newline at end of file
+(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=e-new Date,i=n<0,o=Math.floor(Math.abs(n)/1e3),r=Math.floor(o/60),s=Math.floor(r/60),a=Math.floor(s/24);if(0===r)return"Just now";let c="";if(o<10)c="a moment";else if(o<60)c="less than a minute";else if(r<5)c="a few minutes";else if(s<24)c=0===s?`${r} ${1===r?"minute":"minutes"}`:`${s} ${1===s?"hour":"hours"}`;else{if(!(a<7))return e.toLocaleDateString();c=`${a} ${1===a?"day":"days"}`}return i?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.icon=null,window.getIcon=function(t,e=""){if(void 0===t)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return e=""!==e&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${e.slice(0,2)}`:"",n.classList.add(`icon-${t}${e}`),n},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.generateID=function(t="jvb"){return`${t}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(t,e,n,i="",o=""){const r=e<n;t.progress&&r&&window.fade(t.progress,!0);const s=n>0?e/n*100:0;t.fill&&(t.fill.style.width=`${s}%`),t.details&&(t.details.textContent=i),t.count&&(t.count.textContent=`${e}/${n}`),t.icon&&(t.icon.className=""===o?"icon":"icon icon-"+o),t.progress&&e===n&&window.fade(t.progress,!1)},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let s=!0;return async function(){for(;s;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){s=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return Array.isArray(e)&&(e=e.join(",")),"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const s=this.map(t[o],r);null!==s&&(s.hasOwnProperty("type")&&s.hasOwnProperty("data")?n[o]=s.data:n[o]=s,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}};document.body;const t=document.documentElement,e=document.querySelector(".scroll-progress .bar");let n=window.scrollY||t.scrollTop||0,i=-1,o=!1,r=0;function s(){r=Math.max(0,t.scrollHeight-window.innerHeight)}function a(t){if(!e)return;const n=r>0?t/r:0,i=Math.max(0,Math.min(1,n));e.style.transform=`scaleX(${i})`}function c(){const e=window.scrollY||t.scrollTop||0;e>n?i=1:e<n&&(i=-1),n=e,document.body.classList.toggle("scroll-up",i<0&&e>0),a(e),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(c))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{s(),a(window.scrollY||t.scrollTop||0)}),20)})),s(),a(n)})();
\ No newline at end of file
diff --git a/base/_setup.php b/base/_setup.php
index e8d7871..9e46047 100644
--- a/base/_setup.php
+++ b/base/_setup.php
@@ -18,3 +18,14 @@
$childURL = apply_filters('jvbChildUrl', JVB_URL);
define('JVB_CHILD_URL', $childURL);
+
+
+function jvbDefaultIcon():string
+{
+ return apply_filters('jvbDefaultIcon', 'arrows-clockwise');
+}
+
+function jvbLogoIcon():string
+{
+ return apply_filters('jvbLogoIcon', 'logo');
+}
diff --git a/build/drawer-menu/block.json b/build/drawer-menu/block.json
new file mode 100644
index 0000000..605e348
--- /dev/null
+++ b/build/drawer-menu/block.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/drawer-menu",
+ "title": "Drawer Menu",
+ "category": "jvb",
+ "icon": "menu",
+ "version": "1.0.0",
+ "textdomain": "jvb",
+ "supports": {
+ "html": false
+ },
+ "attributes": {
+ "menuId": {
+ "type": "string",
+ "default": ""
+ },
+ "collapsed": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
\ No newline at end of file
diff --git a/build/drawer-menu/index-rtl.css b/build/drawer-menu/index-rtl.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/drawer-menu/index-rtl.css
@@ -0,0 +1 @@
+
diff --git a/build/drawer-menu/index.asset.php b/build/drawer-menu/index.asset.php
new file mode 100644
index 0000000..8180b18
--- /dev/null
+++ b/build/drawer-menu/index.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components'), 'version' => '5613e7295e6abf1d1090');
diff --git a/build/drawer-menu/index.css b/build/drawer-menu/index.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build/drawer-menu/index.css
@@ -0,0 +1 @@
+
diff --git a/build/drawer-menu/index.js b/build/drawer-menu/index.js
new file mode 100644
index 0000000..687097a
--- /dev/null
+++ b/build/drawer-menu/index.js
@@ -0,0 +1 @@
+(()=>{"use strict";var e,r={582:()=>{const e=window.wp.blocks,r=window.wp.blockEditor,n=window.wp.components,o=window.ReactJSXRuntime;(0,e.registerBlockType)("jvb/drawer-menu",{edit:function({attributes:e,setAttributes:t}){const{menuId:l,collapsed:s}=e,i=(0,r.useBlockProps)();return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(r.InspectorControls,{children:(0,o.jsxs)(n.PanelBody,{title:"Drawer Settings",children:[(0,o.jsx)(n.TextControl,{label:"Menu ID",value:l,onChange:e=>t({menuId:e}),help:"PHP-generated menu identifier"}),(0,o.jsx)(n.ToggleControl,{label:"Start Collapsed",checked:s,onChange:e=>t({collapsed:e})})]})}),(0,o.jsx)("div",{...i,children:(0,o.jsxs)("div",{className:"drawer-menu-preview",children:[(0,o.jsxs)("p",{children:["Drawer Menu: ",l||"Not configured"]}),(0,o.jsxs)("p",{children:["State: ",s?"Collapsed":"Expanded"]})]})})]})},save:function(){return null}})}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var l=n[e]={exports:{}};return r[e](l,l.exports,o),l.exports}o.m=r,e=[],o.O=(r,n,t,l)=>{if(!n){var s=1/0;for(c=0;c<e.length;c++){for(var[n,t,l]=e[c],i=!0,a=0;a<n.length;a++)(!1&l||s>=l)&&Object.keys(o.O).every((e=>o.O[e](n[a])))?n.splice(a--,1):(i=!1,l<s&&(s=l));if(i){e.splice(c--,1);var d=t();void 0!==d&&(r=d)}}return r}l=l||0;for(var c=e.length;c>0&&e[c-1][2]>l;c--)e[c]=e[c-1];e[c]=[n,t,l]},o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),(()=>{var e={121:0,397:0};o.O.j=r=>0===e[r];var r=(r,n)=>{var t,l,[s,i,a]=n,d=0;if(s.some((r=>0!==e[r]))){for(t in i)o.o(i,t)&&(o.m[t]=i[t]);if(a)var c=a(o)}for(r&&r(n);d<s.length;d++)l=s[d],o.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return o.O(c)},n=globalThis.webpackChunkjvb=globalThis.webpackChunkjvb||[];n.forEach(r.bind(null,0)),n.push=r.bind(null,n.push.bind(n))})();var t=o.O(void 0,[397],(()=>o(582)));t=o.O(t)})();
\ No newline at end of file
diff --git a/build/drawer-menu/render.php b/build/drawer-menu/render.php
new file mode 100644
index 0000000..d0826a3
--- /dev/null
+++ b/build/drawer-menu/render.php
@@ -0,0 +1,39 @@
+<?php
+
+use JVBase\managers\CacheManager;
+use JVBase\ui\Navigation;
+
+$menu_id = $attributes['menuId'] ?? '';
+$collapsed = $attributes['collapsed'] ?? true;
+
+// You'd populate this from options, a filter, or however you store menu data
+$menu_items = apply_filters('jvbDrawerItems', [], $menu_id);
+
+if (empty($menu_items) || empty($menu_id)) {
+ return '<p>Please configure the drawer menu in block settings.</p>';
+}
+
+$cache = CacheManager::for('drawer');
+$cache->clear();
+
+if (!is_front_page()) {
+ $menu_items[] = [
+ 'text' => 'Home',
+ 'url' => home_url(),
+ 'icon' => 'house-simple',
+ ];
+}
+$items = array_map(function($item) { return $item['text'];}, $menu_items);
+
+$key = $cache->generateKey($items);
+$menu = $cache->remember($key,
+function () use ($menu_items, $menu_id, $collapsed) {
+ $menu = new Navigation($menu_id);
+ $menu->asDrawer($collapsed)->populateFromArray($menu_items);
+ return $menu->render();
+});
+
+global $wp;
+
+$current = home_url($wp->request.'/');
+echo str_replace($current.'"', $current.'" class="current" aria-current="page"', $menu);
diff --git a/build/drawer-menu/style-index-rtl.css b/build/drawer-menu/style-index-rtl.css
new file mode 100644
index 0000000..2527c65
--- /dev/null
+++ b/build/drawer-menu/style-index-rtl.css
@@ -0,0 +1 @@
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;left:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:var(--base);border-right:1px solid var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(-180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid var(--contrast-200)}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
diff --git a/build/drawer-menu/style-index.css b/build/drawer-menu/style-index.css
new file mode 100644
index 0000000..1591a7f
--- /dev/null
+++ b/build/drawer-menu/style-index.css
@@ -0,0 +1 @@
+nav.drawer{bottom:0;max-height:100vh;overflow:hidden auto;position:fixed;right:0;transition:var(--trans-size);width:var(--btn);z-index:var(--z-5);--dir:column-reverse;background-color:var(--base);border-left:1px solid var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-4)) var(--shdw-left);height:auto;--w:var(--chip_)}nav.drawer .section-title,nav.drawer .title{display:none}nav.drawer ul .icon{min-width:var(--chip_)}nav.drawer .a,nav.drawer a{gap:.5rem;height:var(--chipchip);justify-content:center;padding:0 .5rem;width:100%}nav.drawer .toggle{width:100%}nav.drawer .toggle .icon{transform:rotate(0);transition:var(--trans-transform)}nav.drawer.open{width:240px}nav.drawer.open .section-title,nav.drawer.open .title{display:block}nav.drawer.open .toggle .icon{transform:rotate(180deg)}nav.drawer.open .a,nav.drawer.open a{justify-content:flex-start}nav.drawer ul{--dir:column;--gap:0;--height:auto;margin:0;padding:0}nav.drawer li,nav.drawer ul{height:auto;width:100%}nav.drawer .row{width:100%}nav.drawer .menu-section{border-bottom:1px solid var(--contrast-200)}nav.drawer .section-title{font-size:var(--small);font-weight:700;opacity:.6;padding:.5rem var(--px);text-transform:uppercase}
diff --git a/build/feed/style-index-rtl.css b/build/feed/style-index-rtl.css
index 22875c2..ba5c5ec 100644
--- a/build/feed/style-index-rtl.css
+++ b/build/feed/style-index-rtl.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .feed-filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{right:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--trans-base),filter var(--trans-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] summary span:last-of-type{right:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-3));border-radius:var(--radius);bottom:0;right:0;padding:.25rem 1.1rem .25rem .25rem;position:absolute;left:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;left:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{right:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{right:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));right:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;left:50%;text-align:left}.feed.item[data-timeline] .images span:last-of-type{right:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-left:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
diff --git a/build/feed/style-index.css b/build/feed/style-index.css
index bbc8a27..7429b47 100644
--- a/build/feed/style-index.css
+++ b/build/feed/style-index.css
@@ -1 +1 @@
-.feed-block{grid-column:full}.feed-block .feed-filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{left:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item img{filter:grayscale(.5) sepia(.3) blur(7px);opacity:.7;transition:opacity var(--trans-base),filter var(--trans-base)}.feed.item img[data-loaded=true]{filter:none;opacity:1}.feed.item[data-timeline]{aspect-ratio:unset}.feed.item[data-timeline] summary{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] summary span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] summary span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] summary span:last-of-type{left:50%;top:0}.feed.item[data-timeline] summary>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item details a{font-size:clamp(1rem,.9306rem + .2222vw,1.125rem)}.feed.item.highlighted{animation:highlight-puls 2s ease-in-out;box-shadow:0 0 0 4px #ff0080,0 8px 16px rgba(0,0,0,.1)}.feed.item:hover .handle,.feed.item[open] .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:var(--overlay-pink-medium)}.feed.item summary{aspect-ratio:1;height:100%;width:calc(100% - 1rem)}.feed.item summary .handle{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-3));border-radius:var(--radius);bottom:0;left:0;padding:.25rem .25rem .25rem 1.1rem;position:absolute;right:0;z-index:1}.feed.item summary:after{bottom:.35rem;cursor:pointer;height:1.5rem;position:absolute;right:.7rem;width:1.5rem;z-index:11}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
+.feed-block{grid-column:full}.feed-block .filters{margin:0 auto;max-width:var(--wide);padding:1rem 0}.feed-block .filter-group{padding:2rem 0;position:relative}.feed-block .filter-group .label{left:0;position:absolute}.feed-block .filter-group>.label{top:0}.feed-block .filter-group [type=radio]{left:var(--offScreen);position:absolute}.feed-block .filter-group button,.feed-block .filter-group label{height:-moz-max-content;height:max-content;padding:.5rem;position:relative}.feed-block .filter-group button:hover,.feed-block .filter-group label:hover{color:var(--action-contrast)}.feed-block .filter-group :checked+label .label,.feed-block .filter-group button:hover .label,.feed-block .filter-group label:hover .label{opacity:1;visibility:visible}.feed-block .filter-group button .label,.feed-block .filter-group label .label,.feed-block .filter-group:has(label:hover) :checked+label .label{--height:max-content;bottom:-2rem;font-weight:var(--fw-b);opacity:0;visibility:hidden;white-space:nowrap;width:-moz-max-content;width:max-content}.feed-block h3{font-size:var(--medium);margin:0 0 .25rem}.placeholder{align-items:center;aspect-ratio:1;background:var(--base);border:1rem solid var(--base-50);border-radius:1rem;display:flex;justify-content:center}.placeholder i.icon{--w:50%;animation:dance 2.5s ease-in-out infinite;color:var(--base-200)}.item-grid{max-width:none;padding:0 var(--chip)}.feed.item{background:var(--base-50);border-radius:.5rem;box-shadow:0 2px 4px rgba(0,0,0,.1);height:-moz-fit-content;height:fit-content;overflow:hidden;padding:0;position:relative}.feed.item details{padding:0;position:relative;width:100%;z-index:var(--z-2)}.feed.item details summary{backdrop-filter:blur(5px);background-color:rgba(var(--base-rgb),var(--op-2));left:0;position:absolute;top:-3rem;width:100%}.feed.item details summary:hover{background-color:rgba(var(--action-rgb),var(--op-45))}.feed.item details[open]{padding:.25rem .5rem}.feed.item details[open] summary .icon{opacity:0}.feed.item img{-o-object-fit:cover;object-fit:cover}.feed.item img:hover{opacity:.8}.feed.item[data-timeline] .images{aspect-ratio:3/2;padding:0 0 1rem}.feed.item[data-timeline] .images span{background-color:var(--action-0);color:var(--action-contrast);padding:.25rem .5rem;position:absolute;width:50%}.feed.item[data-timeline] .images span:first-of-type{bottom:0;right:50%;text-align:right}.feed.item[data-timeline] .images span:last-of-type{left:50%;top:0}.feed.item[data-timeline] .images>a{display:flex;flex-wrap:nowrap;height:100%;position:relative;width:100%}.feed.item[data-timeline] img{height:100%;-o-object-fit:cover;object-fit:cover;width:50%}.feed.item[data-timeline] img:first-of-type{border-right:1px solid var(--action-0)}.feed.item a:after,.feed.item a:before{display:none}.feed.item label{font-weight:400;text-transform:none}.feed.item label .icon{--w:1.5em}.item-grid:has([data-timeline]){grid-template-columns:repeat(1,1fr)}@media(min-width:768px){.item-grid:has([data-timeline]){grid-template-columns:repeat(2,1fr)}}
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index 5f0d1c5..c1f2e24 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '9b079150b16e2c23daab');
+<?php return array('dependencies' => array(), 'version' => '39f29851e15768a69790');
diff --git a/build/feed/view.js b/build/feed/view.js
index a550270..c0a3d9e 100644
--- a/build/feed/view.js
+++ b/build/feed/view.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=this.ui.filterContainer.querySelectorAll('[name="content"]'),this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0});this.store=e.feed,this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&window.removeChildren(this.ui.grid),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.observeImages(this.ui.grid),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length,this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length}createItemElement(e){let t=window.getTemplate("feed-item");return Object.hasOwn(t.dataset,"timeline")?this.createTimelineElement(e,t):t}createTimelineElement(e,t){var i,s;let[r,a,o,n,l,d,h,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,a.href,o.src,o.dataset.small,o.dataset.medium,n.src,n.dataset.small,n.dataset.medium,l.textContent,d.textContent,h.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver((()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}loadImage(e){const t=this.getAppropriateImageSize(e);t&&t!==e.src&&(e.src=t,e.dataset.loaded="true")}getAppropriateImageSize(e){return window.innerWidth<768&&e.dataset.small?e.dataset.small:e.dataset.medium?e.dataset.medium:e.src}observeImages(e){e.querySelectorAll("img[data-small], img[data-medium]").forEach((e=>{e.dataset.loaded||this.imageObserver.observe(e)}))}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.container=document.querySelector("section.feed-block"),this.container&&(this.a11y=window.jvbA11y,this.cache=new window.jvbCache("feed"),this.error=window.jvbError,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.initElements(),this.initFilters(),this.loadWhenAble())}loadWhenAble(){"requestIdleCallback"in window?requestIdleCallback((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),{timeout:2e3}):setTimeout((()=>{this.initTaxonomies(),this.initStore(),this.initListeners(),this.initGallery()}),100)}initElements(){var e;this.currentTaxonomies=new Set,this.taxonomyFilters={},this.elements={filterTrigger:"[data-filter]",filters:{content:'[data-filter="content"]',orderby:'[data-filter="orderby"]',order:'[data-filter="order"]',match:'[data-filter="match"]',favourites:'[data-filter="favourites"]',taxonomy:'[data-filter^="taxonomy"]'},selectedTax:".selected-items",clearFilter:"button.clear-filters",loadMore:"button.load-more",filterContainer:".filters",grid:".item-grid"},this.ui=window.uiFromSelectors(this.elements),this.ui.content=null!==(e=this.ui.filterContainer.querySelectorAll('[name="content"]'))&&void 0!==e&&e,this.ui.taxonomies=this.ui.filterContainer.querySelectorAll("[data-taxonomy]"),this.ui.content&&this.ui.content.length>0?this.contentTypes=Array.from(this.ui.content).map((e=>e.value)):this.contentTypes=[this.container.dataset.content],this.ui.taxonomies.length>0?this.taxonomies=Array.from(this.ui.taxonomies).map((e=>e.dataset.taxonomy)):this.taxonomies=[]}async initTaxonomies(){this.selector=window.jvbSelector;const e=document.querySelectorAll('[data-filter="taxonomy"]');this.selector.isInitializing=!0,e.forEach((e=>{const t=e.dataset.taxonomy;this.currentTaxonomies.add(t),this.selector.registerFilterButton(e,{button:e,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax}),this.addTaxonomyPreloadListeners(e,t)})),this.selector.isInitializing=!1,this.selector.subscribe(((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)}))}addTaxonomyPreloadListeners(e,t){const i=()=>{this.selector.preloadTaxonomy(t)};e.addEventListener("mouseenter",i,{once:!0}),e.addEventListener("pointerdown",i,{once:!0}),e.addEventListener("focus",i,{once:!0})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;t.size>0?this.taxonomyFilters[i]=Array.from(t.keys()):delete this.taxonomyFilters[i];let s={page:1};Object.keys(this.taxonomyFilters).length>0&&(s.taxonomy=this.taxonomyFilters),this.updateFilter(s)}clearAllTaxonomies(){this.taxonomyFilters={},window.removeChildren(this.ui.selectedTax),this.updateFilter({taxonomy:null,page:1})}initFilters(){this.filters={content:this.contentTypes[0],orderby:"date",order:"desc",page:1},this.config.context&&(this.filters.context=this.config.context),this.config.source&&(this.filters.source=this.config.source),this.processCachedFilters(),this.processURLFilters(),this.syncUIToFilters()}syncUIToFilters(){this.ui.filterContainer&&Object.entries(this.filters).forEach((([e,t])=>{const i=this.ui.filterContainer.querySelector(`[data-filter="${e}"][value="${t}"]`);i&&(i.checked=!0)})),this.updateContentFor(this.filters.content)}nextPage(){this.store.setFilter("page",this.store.filters.page++)}initStore(){const e=window.jvbStore.register("feed",{storeName:"feed",endpoint:"feed",keyPath:"id",indexes:[{name:"content",keyPath:"content"},{name:"taxonomy",keyPath:"taxonomy"},{name:"user",keyPath:"user"},{name:"date",keyPath:"modified"},{name:"title",keyPath:"title"}],filters:this.filters,TTL:216e5,showLoading:!0,required:"content",delayFetch:!0});this.store=e.feed,this.store.subscribe(((e,t)=>{"data-loaded"===e&&(this.renderItems(),this.ui.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse.has_more&&(this.ui.loadMore.hidden=!this.store.lastResponse.has_more))}))}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe(((e,t)=>{"load-more"===e&&this.store.lastResponse&&this.store.lastResponse.has_more&&this.nextPage()}))}processCachedFilters(){Object.keys(this.filters).forEach((e=>{let t=this.cache.get(`${this.config.source}_${this.config.context}_${e}`);t&&t!==this.filters[e]&&(this.filters[e]=t)}))}processURLFilters(){if(this.filters.page>1)return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;["content","order","orderby","favourites","match"].forEach((t=>{let i=e.get(`f_${t}`);if(i){this.filters[t]=i;let e=this.ui.filters[t];e&&(e.checked=!0)}}));let t=!1;if(e.forEach(((e,i)=>{if(i.startsWith("f_tax_")){t=!0;const s=i.replace("f_tax_","");this.taxonomyFilters[s]||(this.taxonomyFilters[s]=[]),this.taxonomyFilters[s]=e.split(",").map(Number)}})),this.ui.filterContainer&&t)for(let[e,t]in Object.entries(this.taxonomyFilters)){let i=this.ui.filterContainer.querySelector(`[data-taxonomy="${e}"]`);i&&(i.dataset.fieldId?(this.selector.get(i.dataset.fieldId).selectedTerms=new Set(t),this.selector.initFieldDisplay(i.dataset.fieldId)):this.selector.registerField(i,{button:i,buttonSelector:'[data-filter="taxonomy"]',selected:this.ui.selectedTax,selectedItems:t}))}return!0}updateURL(){const e=new URLSearchParams;["content","order","orderby","match"].forEach((t=>{this.filters[t]&&e.set(`f_${t}`,this.filters[t])})),Object.entries(this.taxonomyFilters).forEach((([t,i])=>{i.length>0&&e.set(`f_tax_${t}`,i.join(","))}));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;window.history.pushState({filters:this.filters},"",t)}renderItems(){let e=this.store.getFiltered();if(1===this.store.filters.page&&window.removeChildren(this.ui.grid),0===e.length)return void this.a11y.announceItems(0,this.store.filters.page>0);const t=document.createDocumentFragment(),i=s=>{const r=Math.min(s+10,e.length);for(let i=s;i<r;i++){const s=e[i],r=this.createItemElement(s);t.appendChild(r)}r<e.length?requestAnimationFrame((()=>i(r))):(this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.updateGalleryItems(this.gallery.getGalleryItems()),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,this.store.filters.page>1,this.store.hasMore))};e.length>0?i(0):this.a11y.announceItems(0,this.store.filters.page>1,!1),this.ui.filters.match&&(this.ui.filters.match.hidden=0===Object.keys(this.taxonomyFilters).length),this.ui.clearFilter&&(this.ui.clearFilter.hidden=0===Object.keys(this.taxonomyFilters).length)}createItemElement(e){let t=window.getTemplate(`feedItem${window.uppercaseFirst(e.content)}`);if(Object.hasOwn(t.dataset,"timeline"))return this.createTimelineElement(e,t);for(let[i,s]of Object.entries(e.fields)){let r=t.querySelector(`[data-field="${i}"]`);r&&(Object.keys(e.images).map((e=>parseInt(e))).includes(s)?[r.src,r.srcset,r.alt]=[e.images[s].tiny,`${e.images[s].tiny} 50w, ${e.images[s].small} 300w, ${e.images[s].medium} 1024w`,e.images[s]["image-alt-text"]]:"TIME"===r.tagName?(r.setAttribute("datetime",s),r.textContent=window.formatTimeAgo(s)):r.textContent=s,""===s&&r.remove())}let i=t.querySelector("a");var s;return i&&""!==e.url&&([i.href,i.title]=[e.url,`View ${null!==(s=e.fields.post_title)&&void 0!==s?s:"Item"}`]),t}createTimelineElement(e,t){var i,s;let[r,n,a,o,l,h,d,c,m,u]=[t,t.querySelector("a"),t.querySelector("img.before"),t.querySelector("img.after"),t.querySelector("summary span:last-of-type"),t.querySelector("p.started time"),t.querySelector("p.updated time"),t.querySelector("p.total b"),t.querySelector(".term-list"),Object.values(e.fields.order)],f=u.length-1,y=e.images[u[0].post_thumbnail],g=e.images[u[f].post_thumbnail];return[r.dataset.id,n.href,a.src,a.dataset.small,a.dataset.medium,o.src,o.dataset.small,o.dataset.medium,l.textContent,h.textContent,d.textContent,c.textContent]=[e.id,e.url,y.tiny,y.small,y.medium,g.tiny,g.small,g.medium,`${l.textContent} ${f} Tx`,null!==(i=u[0].date)&&void 0!==i?i:e.date,null!==(s=u[f].date)&&void 0!==s?s:"",`${f} Treatments`],t}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach((e=>e.remove()))}addPlaceholders(){let e=this.contentTypes.length;const t=document.createDocumentFragment();for(let i=0;i<12;i++){let i,s=window.getTemplate("placeholderTemplate"),r=Math.floor(Math.random()*e);i=this.ui.content&&this.ui.content.length>0?this.ui.content.filter((e=>e.value===this.contentTypes[r])).querySelector(".icon").cloneNode(!0):window.getIcon(this.container.dataset.icon),s.append(i),t.append(s)}this.ui.grid.append(t)}updateFilter(e){let t=["taxonomy","favourites","match",...Object.keys(this.filters)];e=Object.keys(e).filter((e=>t.includes(e))).reduce(((t,i)=>(t[i]=e[i],t)),{}),window.getDifferences.map(this.filters,e)&&(this.filters={...this.filters,...e},this.updateURL(),this.store.setFilters(e))}updateContentFor(e){this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]').forEach((t=>{const i=t.dataset.for?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)})),this.ui.filterContainer.querySelectorAll("[data-for]").forEach((t=>{const i=t.dataset.for?.split(",")||[];i.length>0&&(t.hidden=!i.includes(e),t.hidden&&t.checked&&(t.checked=!1))}));const t=this.ui.filterContainer.querySelector('[name="orderby"]:checked');this.updateOrderDirectionVisibility(t?.value)}updateOrderDirectionVisibility(e){const t=this.ui.filterContainer.querySelector(".order-direction");if(t){const i=t.dataset.forOrder?.split(",")||[];t.hidden=i.length>0&&!i.includes(e)}}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.imageObserver=null,this.resizeObserver=null,"IntersectionObserver"in window&&(this.imageObserver=new IntersectionObserver((e=>{e.forEach((e=>{this.loadImage(e.target),this.imageObserver.unobserve(e.target)}))}),{rootMargin:"100px",threshold:.1})),"ResizeObserver"in window?this.resizeObserver=new ResizeObserver((()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})):window.addEventListener("resize",(()=>{window.debouncer.schedule("feed-update-images",(()=>this.updateImageSizes()),250)})),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}handlePopState(e){e.state?.filters&&this.processURLFilters()&&(this.store.setFilters(this.filters),this.a11y.announce("Feed filters updated from browser history"))}handleClick(e){window.targetCheck(e,this.elements.loadMore)?this.nextPage():window.targetCheck(e,this.elements.clearFilter)?this.clearAllTaxonomies():window.targetCheck(e,".remove-item")&&this.handleRemoveSelectedTerm(e)}handleRemoveSelectedTerm(e){const t=e.target.closest(".selected-item");if(!t)return;const i=parseInt(t.dataset.id),s=t.dataset.taxonomy;this.taxonomyFilters[s]&&(this.taxonomyFilters[s]=this.taxonomyFilters[s].filter((e=>e!==i)),0===this.taxonomyFilters[s].length&&delete this.taxonomyFilters[s]),t.remove(),this.updateFilter({taxonomy:Object.keys(this.taxonomyFilters).length>0?this.taxonomyFilters:null,page:1})}handleChange(e){let t=e.target;Object.hasOwn(t.dataset,"filter")&&("content"===t.dataset.filter?(this.updateContentFor(t.value),this.updateFilter({content:t.value,page:1})):"orderby"===t.dataset.filter?(this.updateOrderDirectionVisibility(t.value),this.updateFilter({orderby:t.value,page:1})):"order"===t.dataset.filter?this.updateFilter({order:t.value,page:1}):"match"===t.dataset.filter?this.updateFilter({match:t.checked?"all":"any",page:1}):"favourites"===t.dataset.filter&&this.updateFilter({favourites:t.checked,page:1}))}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.feedBlock=new e)}))}))})();
\ No newline at end of file
diff --git a/build/forms/view.asset.php b/build/forms/view.asset.php
index 14684c2..5ac10d4 100644
--- a/build/forms/view.asset.php
+++ b/build/forms/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '918c373929631a4cc439');
+<?php return array('dependencies' => array(), 'version' => '2321e606c22ff3ca6cff');
diff --git a/build/forms/view.js b/build/forms/view.js
index ac99e02..6d7c3b2 100644
--- a/build/forms/view.js
+++ b/build/forms/view.js
@@ -1 +1 @@
-(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){let r=o.formId,e=o.config,t=o.fullData,n=e.element;const s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));window.jvbUploads&&(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r,uploadId:e,meta:t})=>{s.append(`${r}[]`,o)}));let l={};n.closest(".jvb-form-block"),this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",headers:l,body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
+(()=>{class o{constructor(){this.controller=new window.jvbForm,document.querySelectorAll(".jvb-form-block form").forEach((o=>{this.controller.registerForm(o,{autosave:!0,autoUpload:!1})})),this.controller.subscribe(((o,r)=>{"form-submit"===o&&this.handleFormSubmission(r)}))}async handleFormSubmission(o){const{formId:r,config:e,fullData:t}=o,n=e.element,s=new FormData;for(const[o,r]of Object.entries(t))"_wpnonce"!==o&&"_wp_http_referer"!==o&&(Array.isArray(r)?r.forEach((r=>s.append(`${o}[]`,r))):"object"==typeof r&&null!==r?s.append(o,JSON.stringify(r)):s.append(o,r));if(window.jvbUploads)try{(await window.jvbUploads.getFilesForForm(n)).forEach((({file:o,fieldName:r})=>{s.append(`${r}[]`,o)}))}catch(o){console.error("Error getting files:",o)}this.controller.showFormStatus(r,"uploading");try{const o=await fetch(`${jvbSettings.api}forms`,{method:"POST",credentials:"same-origin",body:s}),e=await o.json();if(!o.ok)return this.controller.showFormStatus(r,"error"),void this.controller.handleFormError(n,e);if(this.controller.showFormStatus(r,"submitted"),this.controller.showSummary(r,".jvb-form-block"),window.jvbUploads){const o=n.querySelectorAll("[data-upload-field]");for(const r of o){const o=window.jvbUploads.determineFieldId(r);await window.jvbUploads.clearFieldFromStores(o)}}}catch(o){console.error("Form submission error:",o),this.controller.showFormStatus(r,"error"),this.controller.handleFormError(n,{message:"Network error. Please check your connection and try again.",code:"network_error"})}finally{await this.controller.store.delete(r)}}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((r=>{"auth-loaded"===r&&new o}))}))})();
\ No newline at end of file
diff --git a/build/list/render.php b/build/list/render.php
index 2b56c65..f8ef568 100644
--- a/build/list/render.php
+++ b/build/list/render.php
@@ -169,7 +169,7 @@
$out .= '</section>';
echo $out;
- echo jvbBuildDirectoryNavigation();
+ echo JVB()->directories()?->renderNavigation();
return ob_get_clean();
}
diff --git a/build/summary/render.php b/build/summary/render.php
index d6a2040..4c22c7a 100644
--- a/build/summary/render.php
+++ b/build/summary/render.php
@@ -44,7 +44,7 @@
$artist = jvbContentFromUser((int)$current->post_author);
$sections = JVB_CONTENT[jvbNoBase($current->post_type)]['sections']??[];
- jvbDump($sections);
+
// $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 52b25d1..f83b57d 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -19,7 +19,6 @@
$this->cache = CacheManager::for('blocks', WEEK_IN_SECONDS);
add_filter('render_block', [$this, 'render'], 999, 3);
-
add_action('init', [$this, 'registerBlockStyles']);
}
@@ -126,7 +125,7 @@
*/
- protected function render_core_button(array $block):string
+ public function render_core_button(array $block):string
{
preg_match('/href="([^"]*)"/', $block['innerHTML'], $url);
preg_match('/>([^<]*)<\/a>/', $block['innerHTML'], $label);
@@ -160,13 +159,13 @@
);
}
- protected function render_core_buttons(array $block):string
+ public function render_core_buttons(array $block):string
{
return '<ul'.$this->getClassesAndStyles($block['attrs'], ['buttons','row']).'>'.
$this->innerBlocks($block).'</ul>';
}
- protected function render_core_column(array $block):string
+ public function render_core_column(array $block):string
{
$styles = (array_key_exists('attrs', $block) &&
array_key_exists('width', $block['attrs'])) ?
@@ -177,7 +176,7 @@
$this->innerBlocks($block).'</div>';
}
- protected function render_core_columns(array $block):string
+ public function render_core_columns(array $block):string
{
return '<section'.
$this->getClassesAndStyles($block['attrs'], ['columns']).'>'.
@@ -185,7 +184,7 @@
}
//core_comment_template
- protected function render_core_group(array $block):string
+ public function render_core_group(array $block):string
{
$tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
@@ -198,12 +197,12 @@
//core_more
//core_nextpage
- protected function render_core_separator(array $block):string
+ public function render_core_separator(array $block):string
{
return '<hr'.$this->getClassesAndStyles($block['attrs']).'>';
}
- protected function render_core_spacer(array $block):string
+ public function render_core_spacer(array $block):string
{
return '<div'.$this->getClassesAndStyles($block['attrs'], ['spacer'], ['height:2rem']).
' aria-hidden="true"></div>';
@@ -219,7 +218,7 @@
* Media Blocks
*/
//core_audio
- protected function render_core_cover(array $block):string
+ public function render_core_cover(array $block):string
{
// Extract block attributes
@@ -254,7 +253,7 @@
// Build classes and styles
unset($attrs['url']);
- $classes = $this->getClassesAndStyles($attrs, ['cover']);
+ $classes = $this->getClassesAndStyles($attrs, ['cover row']);
return '<section' . $classes . '>' .
@@ -266,14 +265,14 @@
//core_file
- protected function render_core_gallery(array $block):string
+ public function render_core_gallery(array $block):string
{
return '<ul'.$this->getClassesAndStyles($block['attrs'], ['gallery']).'>'.
$this->innerBlocks($block,'<li>', '</li>').
'</ul>';
}
- protected function render_core_image(array $block):string
+ public function render_core_image(array $block):string
{
$ID = $this->imageID('', $block);
if (!$ID) {
@@ -294,7 +293,7 @@
$caption.'</figure>';
}
- protected function render_core_media_text(array $block):string
+ public function render_core_media_text(array $block):string
{
$ID = $this->imageID('', $block);
@@ -334,7 +333,7 @@
//render_core_details
//render_core_footnotes
//render_core_classic
- protected function render_core_heading(array $block):string
+ public function render_core_heading(array $block):string
{
$level = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : '2';
$content = $this->inside($block);
@@ -344,25 +343,25 @@
'</h'.$level.'>';
}
- protected function render_core_list(array $block):string
+ public function render_core_list(array $block):string
{
$tag = (array_key_exists('ordered', $block['attrs'])) ? 'ol' : 'ul';
return '<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.$this->innerBlocks($block).'</'.$tag.'>';
}
-// protected function render_core_list_item(array $block):string
+// public function render_core_list_item(array $block):string
// {
// return '<li'.$this->getClassesAndStyles($block['attrs']).'>'.$this->inside($block).'</li>';
// }
//render_core_missing
- protected function render_core_paragraph(array $block):string
+ public function render_core_paragraph(array $block):string
{
return '<p'.$this->getClassesAndStyles($block['attrs']).'>'.
$this->inside($block, 'p').
'</p>';
}
- protected function render_core_quote(array $block): string
+ public function render_core_quote(array $block): string
{
$innerHTML = $block['innerHTML'];
@@ -383,7 +382,7 @@
$citeHtml.
'</blockquote>';
}
- protected function render_core_pullquote(array $block): string
+ public function render_core_pullquote(array $block): string
{
$innerHTML = $block['innerHTML'];
@@ -415,7 +414,7 @@
//core_loginout
//core_pattern
- protected function render_core_site_logo(array $block, string $content):string
+ public function render_core_site_logo(array $block, string $content):string
{
$open = $close = '';
@@ -430,7 +429,7 @@
}
//core_site_title_tagline
- protected function render_core_site_title(array $block, string $content):string
+ public function render_core_site_title(array $block, string $content):string
{
$tag = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : 1;
$tag = ($tag == 0) ? 'p' : 'h'.$tag;
@@ -472,7 +471,7 @@
/**
* Theme Navigation Blocks
*/
- protected function render_core_navigation(array $block, string $content):string
+ public function render_core_navigation(array $block, string $content):string
{
$ID = (array_key_exists('ref', $block['attrs'])) ? $block['attrs']['ref'] : false;
@@ -513,7 +512,7 @@
'</nav>'.$helpmenu;
}
- protected function render_core_navigation_link(array $block):string
+ public function render_core_navigation_link(array $block):string
{
global $wp;
$url = (str_starts_with($block['attrs']['url'],'/')) ?
@@ -535,7 +534,7 @@
return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>';
}
- protected function render_core_navigation_submenu(array $block):string
+ public function render_core_navigation_submenu(array $block):string
{
global $wp;
$url = (str_starts_with($block['attrs']['url'],'/')) ?
@@ -605,7 +604,7 @@
//core_post_author
//core_post_author_biography
//core_post_author_name
- protected function render_core_post_content(array $block, string $content = ''):string
+ public function render_core_post_content(array $block, string $content = ''):string
{
$tag = (array_key_exists('tagName', $block['attrs'])) ?
@@ -614,6 +613,7 @@
if ($content == '') {
global $post;
+
$block['innerBlocks'] = parse_blocks($post->post_content);
return $this->innerBlocks($block);
} else {
@@ -621,13 +621,13 @@
}
}
//core_post_date
- protected function render_core_post_date(array $block):string
+ public function render_core_post_date(array $block):string
{
$postDate = get_the_date('c');
return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']).'>'.get_the_date().'</time>';
}
//core_post_excerpt
- protected function render_core_post_featured_image(array $block):string
+ public function render_core_post_featured_image(array $block):string
{
global $post;
$ID = get_post_thumbnail_id($post->ID);
@@ -644,7 +644,7 @@
//core_post_navigation_link
//core_post_template
//core_post_terms
- protected function render_core_post_terms(array $block):string
+ public function render_core_post_terms(array $block):string
{
$terms = get_the_terms(get_the_ID(), $block['attrs']['term']);
$out = '';
@@ -664,7 +664,7 @@
return $out;
}
//core_post_time_to_read
- protected function render_core_post_title(array $block):string
+ public function render_core_post_title(array $block):string
{
$open = $close = '';
if (array_key_exists('isLink', $block['attrs'])) {
@@ -689,9 +689,8 @@
'</h'.$level.'>';
}
- protected function render_core_query(array $block, string $content):string
+ public function render_core_query(array $block, string $content):string
{
-
$queryID = $block['attrs']['queryId'];
$args = [];
$inherit = $block['attrs']['inherit']??false;
@@ -798,7 +797,7 @@
//core_query_pagination_previous
//core_query_title
//core_read_more
- protected function render_core_template_part(array $block, string $content):string
+ public function render_core_template_part(array $block, string $content):string
{
$isHeaderTemplate = (
@@ -877,7 +876,7 @@
//core_rss
//core_search
//core_shortcode
- protected function render_core_social_link(array $block, string $content):string
+ public function render_core_social_link(array $block, string $content):string
{
$url = $block['attrs']['url'];
$service = $block['attrs']['service'];
@@ -888,7 +887,7 @@
}
return '<li><a href="'.$url.'" target="_blank" rel="nofollow" title="Find us on '.ucfirst($service).'">'.$icon.'<span class="screen-reader-text">Find us on '.ucfirst($service).'</span></a></li>';
}
- protected function render_core_social_links(array $block, string $content):string
+ public function render_core_social_links(array $block, string $content):string
{
return '<ul class="socials">'.$this->innerBlocks($block).'</ul>';
}
@@ -1514,7 +1513,6 @@
// Background URL (for cover, media blocks)
case 'url':
- jvbDump($value);
if (!empty($value) && str_starts_with($value, 'http')) {
$styles[] = 'background-image: url('.$value.')';
}
@@ -1750,5 +1748,3 @@
}
}
-
-new CustomBlocks();
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index 7a5158b..8c630dd 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -21,7 +21,7 @@
{
// Initialize cache with connections
$this->cache = CacheManager::for('feed_block', WEEK_IN_SECONDS);
-
+ $this->cache->clear();
// Set up cache connections for all feed content types
$this->setupCacheConnections();
@@ -76,7 +76,7 @@
$config = array_merge($config, $mainConfig['feed']['config']);
} else {
$config['content'] = $content;
- $config['icon'] = JVB_CONTENT[$content]['icon']??['logo-triangle'];
+ $config['icon'] = JVB_CONTENT[$content]['icon']??[jvbLogoIcon()];
}
if (is_singular()) {
$config['source'] = $type->ID;
@@ -126,6 +126,7 @@
public function render(array $attributes, string $content, WP_Block $block)
{
+
$this->config = $this->buildParams($attributes);
return $this->cache->remember(
$this->config,
@@ -134,16 +135,48 @@
}
);
}
+ protected function getContext():string|bool
+ {
+ return array_key_exists('context', $this->config)?$this->config['context']:false;
+ }
+ protected function getIDS():array|bool
+ {
+ return (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? $this->config['ids'] : false;
+ }
+ protected function getClasses():array|bool
+ {
+ return array_key_exists('classes', $this->config) && !empty($this->config['classes']) ? $this->config['classes'] : false;
+ }
+ protected function getSource():string|bool
+ {
+ return array_key_exists('source', $this->config) ? $this->config['source'] : false;
+ }
+
+ protected function getIcon():string|bool
+ {
+ return array_key_exists('icon', $this->config) ? $this->config['icon'] : false;
+ }
+ protected function isGallery():bool
+ {
+ return (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']);
+ }
+ protected function getContent():array|bool
+ {
+ return (array_key_exists('content', $this->config) && !empty ($this->config['content'])) ? $this->config['content'] : false;
+ }
protected function renderBlock(): string
{
- $ids = (array_key_exists('ids', $this->config) && !empty($this->config['ids'])) ? ' id="'.implode(' ',$this->config['ids']).'"' : '';
- $classes = (array_key_exists('classes', $this->config) && !empty($this->config['classes'])) ? ' class="'.implode(' ',$this->config['classes']).'"' : '';
- $source = (array_key_exists('source', $this->config)) ? ' data-source="'.$this->config['source'].'"' : '';
- $context = (array_key_exists('context', $this->config)) ? ' data-context="'.$this->config['context'].'"' : '';
- $icons = (array_key_exists('icon', $this->config)) ? ' data-icon="'.$this->config['icon'].'"' : ' data-icon="logo-triangle"';
- $gallery = (array_key_exists('is_gallery', $this->config) && $this->config['is_gallery']) ? ' data-gallery' : '';
- $content = (array_key_exists('content', $this->config)) ? ' data-content="'.implode(',',$this->config['content']).'"' : '';
+ if (is_post_type_archive(BASE.'directory')) {
+ return '';
+ }
+ $ids = ($this->getIDS()) ? ' id="'.implode(' ',$this->getIDS()).'"' : '';
+ $classes = ($this->getClasses()) ? ' class="'.implode(' ',$this->getClasses()).'"' : '';
+ $source = ($this->getSource()) ? ' data-source="'.$this->getSource().'"' : '';
+ $context = ($this->getContext()) ? ' data-context="'.$this->getContext().'"' : '';
+ $icons = ($this->getIcon()) ? ' data-icon="'.$this->getIcon().'"' : ' data-icon="'.jvbLogoIcon().'"';
+ $gallery = $this->isGallery() ? ' data-gallery' : '';
+ $content = ($this->getContent()) ? ' data-content="'.implode(',',$this->getContent()).'"' : '';
ob_start();
?>
<section<?= $ids.$classes ?> class="feed-block"<?= $content.$source.$context.$gallery.$icons ?>>
@@ -166,9 +199,9 @@
}
$feedContent = $this->getFeedContent();
- $hasMany = count($this->config['content']) > 1;
+ $hasMany = count($this->getContent()) > 1;
?>
- <form class="feed-filters" data-save="feed-<?=$this->config['context']?>">
+ <form class="filters" data-save="feed-<?=$this->getContext()?>">
<?php if ($hasMany) {
//If we have multiple content, only show the content first
?>
@@ -177,7 +210,7 @@
<span class="label">SHOWING: </span>
<?php
$labels = [];
- foreach ($this->config['content'] as $i => $type) :
+ foreach ($this->getContent() as $i => $type) :
$checked = $i === 0 ? ' checked' : '';
$label = $feedContent[$type]['plural'] ?? ucfirst($type);
@@ -224,8 +257,9 @@
<?php endif; ?>
<?php if ($hasMany) { ?>
</summary>
- <?php } ?>
-
+ <?php }
+ if (!empty ($this->config['taxonomies'])) {
+ ?>
<div class="filters">
<div class="filter-group row start">
<span class="label">FILTER BY:</span>
@@ -243,7 +277,7 @@
'feed-'.$tax,
$tax,
[
- 'icon' => $taxConfig['icon']??'logo-triangle',
+ 'icon' => $taxConfig['icon']??jvbLogoIcon(),
'update' => '.selected-items-section .selected-items',
'types' => $contentForTax,
'autocomplete' => false,
@@ -266,7 +300,7 @@
</div>
</div>
</div>
-
+ <?php } ?>
<div class="row btw nowrap">
<div class="order-by filter-group row start w-full">
<span class="label">ORDER BY:</span>
@@ -319,11 +353,11 @@
?>
<div class="item-grid">
<?php
- $total = count($this->config['content']) - 1;
+ $total = count($this->getContent()) - 1;
for ($i = 1; $i <= 36; $i++) {
$rand = rand(0, $total);
- $config = Features::getConfig($this->config['content'][$rand]);
- $icon = jvbIcon($config['icon']??'logo-triangle');
+ $config = Features::getConfig($this->getContent()[$rand]);
+ $icon = jvbIcon($config['icon']??jvbLogoIcon());
?>
<div class="placeholder"><?=apply_filters('jvbFeedPlaceholder', $icon) ?></div>
<?php
@@ -351,47 +385,99 @@
protected function renderTemplates(): void
{
- echo '<template class="feed-item">'.apply_filters('jvbFeedItem', '<details class="item feed" data-umami-event="view_feed">
- <summary class="row btw">
- <span class="handle">DETAILS</span>
- <button class="favourite" title="Add to favourites" onclick="toggleFavourite(this)">
- '.jvbIcon('heart')
- .jvbIcon('heart', ['style'=>'fill']).'
- </button>
- <div class="feed-images">
- <a>
- <img width="300px" height="300px" loading="lazy" decoding="async">
- </a>
- </div>
- </summary>
+ if ($this->getContent()) {
+ foreach ($this->getContent() as $content) {
+ echo $this->getDefaultTemplate($content);
+ }
+ }
- <div class="item-info">
- <h3><a></a></h3>
- <div class="item">
- <span class="label"></span>
- <a></a>
- <p></p>
- </div>
- <div class="item-list">
- <span class="label"></span>
- <ul>
- <li>
- <a></a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </details>', $this->config).'</template>';
- echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="feed-empty-state">
- <h3>NOTHING HERE...</h3>
+ echo '<template class="emptyState">'.apply_filters('jvbFeedEmptyState', '<div class="empty-state">
+ <h3>'.jvbIcon($this->getIcon()).'NOTHING HERE'.jvbIcon($this->getIcon()).'</h3>
<p>Try tweaking those filters a bit.</p>
- <p>Edmonton\'s got talent - let\'s find it.</p>
</div>', $this->config). '</template>';
- echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon('logo-triangle')).'</div></template>';
+ echo '<template class="placeholderTemplate"><div class="placeholder">'.apply_filters('jvbFeedPlaceholder', jvbIcon(jvbLogoIcon())).'</div></template>';
}
+ protected function getFavouritesButton(string $content):string
+ {
+ if (!Features::forSite()->has('favourites') && !Features::forContent($content)->has('favouritable')) {
+ return '';
+ }
+ return '<button class="favourite" type="button" title="Add to favourites" data-action="favourite">
+ '.jvbIcon('heart')
+ .jvbIcon('heart', ['style'=>'fill']).'
+ </button>';
+ }
+ protected function getUpvotesButton(string $content):string
+ {
+ if (!Features::forSite()->has('karma') && !Features::forContent($content)->has('karma')){
+ return '';
+ }
+ return '<div class="karma row">
+ <button type="button" class="vote" data-action="upvote">
+ '.jvbIcon('arrow-fat-up')
+ .jvbIcon('arrow-fat-up', ['style'=>'fill']).
+ '</button>
+ <button type="button" class="vote" data-action="downvote">
+ '.jvbIcon('arrow-fat-down')
+ .jvbIcon('arrow-fat-down', ['style'=>'fill']).
+ '</button>
+ <span class="score"></span>
+ </div>';
+ }
+ protected function getDefaultTemplate(string $content): string
+ {
+ $config = JVB_CONTENT[$content]??[];
+ $hasConfig = array_key_exists('feed', $config);
+ $images = ($hasConfig && array_key_exists('images', $config['feed']) ? $config['feed']['images']:['post_thumbnail']);
+ $images = (is_array($images)) ? $images : [$images];
+ $fields = ($hasConfig && array_key_exists('fields', $config['feed']) ? $config['feed']['fields']:['post_title', 'post_date']);
+ $fields = array_filter($fields, function($field) use($images) {
+ return !in_array($field, $images);
+ });
+ $template = '<div class="feed item col '.$content.'">'.$this->getFavouritesButton($content).$this->getUpvotesButton($content);
+
+ //Add all defined images, but allow for filtering
+ $imageTemplate = '<a>';
+ foreach ($images as $image) {
+ $imageTemplate .= '<img data-field="'.$image.'" width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">';
+ }
+ $imageTemplate .= '</a>';
+ $template .= '<div class="images">'.apply_filters('jvbFeedImages', $imageTemplate, $content, $images).'</div>';
+
+ //Output default fields, but allow for filtering
+ $template .= '<details>
+ <summary>'.apply_filters('jvbFeedItemSummary', jvbIcon('dots-three'), $content).'</summary>';
+
+ $fieldsTemplate = '';
+
+ foreach ($fields as $fieldName) {
+ $fieldType = JVB_CONTENT[$content][$fieldName]['type']??'text';
+ $fieldsTemplate .= apply_filters('jvbFeedItemField', $this->defaultFieldTemplate($fieldType, $fieldName), $content, $fieldName, $fieldType);
+ }
+ $template .= '<div class="item-info">'.apply_filters('jvbFeedItemFields', $fieldsTemplate, $content, $fields).'</div>';
+ $template .= '</details></div>';
+
+ return '<template class="feedItem'.ucfirst($content).'">'.apply_filters('jvbFeedItem', $template, $content).'</template>';
+ }
+
+ protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
+ {
+ $data = ' data-field="'.$fieldName.'"';
+ switch ($fieldName) {
+ case 'post_title':
+ return '<h3'.$data.'></h3>';
+ case 'post_date':
+ case 'post_modified':
+ return '<time'.$data.'></time>';
+ }
+ return match($fieldType) {
+ 'upload' => '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
+ 'taxonomy' => '<ul'.$data.'><li><a><i></i></a></li></ul>',
+ default => '<p'.$data.'></p>',
+ };
+ }
/**
* Get feed content using Features instead of get_option
* Returns array of slug => config for types that show in feed
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index 527e523..6fc6da2 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -54,7 +54,9 @@
{
$this->config = $this->getConfig();
$key = $this->generateKey();
+ $this->cache->clear();
$cache = $this->cache->get($key);
+
if ($cache) {
return $cache;
}
@@ -177,7 +179,7 @@
if (empty($this->type)) {
$this->type = match (true) {
is_tax() => jvbNoBase(get_queried_object()->taxonomy),
- is_post_type_archive() => jvbNoBase(get_post_type()),
+ is_post_type_archive() => jvbNoBase(get_queried_object()->name),
default => jvbNoBase(get_queried_object()->post_type),
};
}
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index 2f0bd4a..c16416b 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -96,7 +96,7 @@
$many = count($terms) > 1;
?>
<li class="<?=$slug?>">
- <?=jvbIcon($config['icon']??'triangle')?>
+ <?=jvbIcon($config['icon']??jvbDefaultIcon())?>
<?php
if ($many) { echo '<ul>'; }
@@ -152,7 +152,7 @@
?>
<div class="info">
<header>
- <h2><?=jvbIcon('logo-triangle')?><?= $title?></h2>
+ <h2><?=jvbIcon(jvbLogoIcon())?><?= $title?></h2>
<?= array_key_exists('post_date', $fields) && $fields['date'] !== '' ? '<time>'.date('F Y', strtotime($fields['post_date'])).'</time>' : '' ?>
<?= array_key_exists('timeline', $fields) && $fields['timeline'] !== '' ? $this->outputTimelineTax($ID) : '' ?>
<?= array_key_exists('post_content', $fields) && $fields['post_content'] !== '' ? '<div class="content">'.wptexturize(wp_kses_post( wpautop($fields['post_content']))).'</div>' : '' ?>
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index 07d0e9c..c6955cf 100644
--- a/inc/blocks/_setup.php
+++ b/inc/blocks/_setup.php
@@ -78,5 +78,8 @@
]
);
}
+ register_block_type(
+ JVB_DIR . '/build/drawer-menu',
+ );
}
add_action('init', 'jvbRegisterBlocks');
diff --git a/inc/forms/TaxonomySelector.php b/inc/forms/TaxonomySelector.php
index e79834d..ab3d7d5 100644
--- a/inc/forms/TaxonomySelector.php
+++ b/inc/forms/TaxonomySelector.php
@@ -32,6 +32,7 @@
$this->id = sanitize_key($id);
$this->taxonomy = jvbCheckBase($taxonomy);
$this->name = jvbNoBase($taxonomy);
+
$this->title = JVB_TAXONOMY[$this->name]['plural'];
$this->base = $config['base']??'';
diff --git a/inc/helpers/breadcrumbs.php b/inc/helpers/breadcrumbs.php
index 020a700..6c6e1f9 100644
--- a/inc/helpers/breadcrumbs.php
+++ b/inc/helpers/breadcrumbs.php
@@ -112,12 +112,11 @@
if (is_singular(BASE.'directory')) {
$type = get_post_meta(get_the_ID(), BASE.'for_type_slug', true);
- return jvbDirectories()[$type] ?? [];
+ return JVB()->directories()?->getDirectoryList()[$type] ?? [];
}
$obj = get_queried_object();
- $directories = jvbDirectories();
-
+ $directories = JVB()->directories()?->directories();
if (is_tax()) {
$tax = jvbNoBase($obj->taxonomy);
return array_key_exists($tax, $directories) ? $directories[$tax] : [];
diff --git a/inc/helpers/directory.php b/inc/helpers/directory.php
index c67f442..178c72a 100644
--- a/inc/helpers/directory.php
+++ b/inc/helpers/directory.php
@@ -6,57 +6,6 @@
function jvbIsDirectory():bool
{
- return (is_post_type_archive(BASE.'directory') || is_singular(BASE.'directory'));
-}
-function jvbDirectoryIds():array
-{
- return array_values(get_option(BASE.'directory_ids'));
-}
-
-function jvbBuildDirectoryNavigation():string
-{
- $nav = get_option(BASE.'directory_nav');
- if ($nav === false) {
- $IDs = jvbGlobalDirectoryInfo();
- $nav = '<nav class="directory-list alignwide" id="directory-list"><ul><li class="title">More Lists:</li>';
- foreach ($IDs as $ID) {
- $bit = (array_key_exists('slug', $ID)) ? $ID['slug'] : $ID['title'];
- $nav .= '<li id="directory-'.$bit.'"><a href="'.$ID['url'].'">[ '.$ID['title'].' ]</a></li>';
- }
- $nav .= '</ul></nav>';
- update_option(BASE.'directory_nav', $nav);
- }
- $ID = get_the_ID();
- return str_replace('directory-'.$ID.'"', 'directory-'.$ID.'" class="current"', $nav);
-}
-
-
-function jvbDirectories($search = 'all'):array
-{
- $get = jvbGlobalDirectoryInfo();
-// jvbDump($get);
-
- if ($search == 'all') {
- return $get;
- } else {
- return $get[$search]??[];
- }
-}
-
-function jvbIndexedDirectories():array
-{
-
- $out = get_transient(BASE.'indexed_directories');
- if (!$out) {
- $get = jvbGlobalDirectoryInfo();
- $out = [];
- foreach ($get as $g) {
- $temp = $g;
- unset($temp['ID']);
- $out[$g['ID']] = $temp;
- }
- set_transient(BASE.'indexed_directories', $out, WEEK_IN_SECONDS);
- }
- return $out;
+ return JVB()->directories() && JVB()->directories()->isDirectory();
}
diff --git a/inc/helpers/media.php b/inc/helpers/media.php
index 6a6f6fe..80c33c7 100644
--- a/inc/helpers/media.php
+++ b/inc/helpers/media.php
@@ -36,3 +36,4 @@
</dialog>
<?php
}
+
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index 859d67e..3f83bff 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -316,17 +316,34 @@
$out = ($label === '') ? '' : '<h2 class="inline">'.$label.'</h2>';
$out .= '<ul class="term-list '.jvbNoBase($terms[array_key_first($terms)]->taxonomy).'">';
foreach ($terms as $term) {
- $out .= '<li>
- <a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="'.$term->name.'">'.
- $term->name.
- '</a>
- </li>';
+ $out .= '<li>'.jvbGetTermLink($term).'</li>';
}
$out .= '</ul>';
return $out;
}
+function jvbGetTermLink(int|WP_Term $term, string $taxonomy = ''):string
+{
+ if (is_int($term)){
+ $term = get_term($term, jvbCheckBase($taxonomy));
+ if (is_wp_error($term)) {
+ return '';
+ }
+ }
+ $cache = CacheManager::for($term->taxonomy);
+ $key = $term->term_id.'-link';
+ return $cache->remember(
+ $key,
+ function() use ($term) {
+ return '<a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="'.$term->name.'">'.
+ $term->name.
+ '</a>';
+ }
+ );
+}
+
+
add_action('wp_footer', 'jvbOutputImageTemplates');
function jvbOutputImageTemplates() {
@@ -367,15 +384,15 @@
<div class="group-header">
<div class="selected">
<div class="field">
- <input type="checkbox" id="select-all-group" name="select-all-group">
- <label for="select-all-group">
+ <input type="checkbox" id="select-all" name="select-all" data-selects="item-grid" data-select-all>
+ <label for="select-all">
Select All In Group
</label>
</div>
<div class="info" hidden>
</div>
</div>
- <div class="group-actions">
+ <div class="selection-actions">
<button type="button" data-action="add-to-group" title="Add selected uploads to this group">
<?= jvbIcon('plus-square') ?>
Add Here
@@ -393,8 +410,8 @@
<div class="fields"></div>
</details>
<div class="group-content col">
+ <p class="hint count"></p>
<div class="item-grid group"></div>
- <p class="hint group-count"></p>
</div>
</div>
@@ -460,10 +477,10 @@
<div class="wrap">
<div class="restore-message">
<h4>Looks like we left things hanging</h4>
- <p class="restore-details"></p>
+ <p class="details"></p>
<p class="hint">If you'd rather start over, you can clear this information.</p>
</div>
- <div class="restore-actions">
+ <div class="selection-actions">
<div class="selected">
<div class="field">
<input type="checkbox" id="select-all-restore" name="select-all-restore">
@@ -493,10 +510,10 @@
</dialog>
</template>
<template class="restoreField">
- <div class="restore-field">
- <h3></h3>
+ <details class="restore-field">
+ <summary><h3><a></a></h3></summary>
<div class="item-grid restore"></div>
- </div>
+ </details>
</template>
<template class="startOverConfirmation">
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index cbfaf3b..bbd289a 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -254,6 +254,13 @@
return $image->formatImage($imgID, $start, $end, $addLink, $postSlug);
}
+function jvbImageCaption(int $imgID, string $start = 'tiny', string $end = 'large', bool $addLink = true, ?string $postSlug = null):string
+{
+ $caption = wp_get_attachment_caption($imgID);
+ $caption = ($caption && $caption !== '') ? '<figcaption>'.apply_filters('the_content', $caption).'</figcaption>' : '';
+ return '<figure>'.jvbFormatImage($imgID, $start, $end, $addLink, $postSlug).$caption.'</figure>';
+}
+
/**
* Outputs the notification container in the footer
* @return void
@@ -425,8 +432,11 @@
<div class="bar">
<div class="fill"></div>
</div>
- <div class="details row btw">
- <?=$inside?>
+ <div class="row btw">
+ <i class="icon"></i>
+ <div class="details">
+ <?=$inside?>
+ </div>
</div>
</div>
<?php
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index 498c2b8..b0b037d 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -157,6 +157,10 @@
*/
protected function registerAdditionalHooks(): void
{
+ $this->ensureInitialized();
+ if (!$this->isSetUp()) {
+ return;
+ }
// User login tracking for security
add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
@@ -527,6 +531,10 @@
*/
public function enqueueScripts(): void
{
+ $this->ensureInitialized();
+ if (!$this->isSetUp()) {
+ return;
+ }
// Helcim JS SDK
$sdk_url = $this->is_test_mode
? 'https://helcim-js-sandbox.helcim.com/v1/helcim.js'
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 12b73ec..b1958bc 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -761,6 +761,12 @@
array $options = []
): array|WP_Error
{
+ if (!$this->is_healthy) {
+ $this->logDebug('Skipping request - integration is unhealthy', [
+ 'consecutive_errors' => $this->error_stats['consecutive_errors'],
+ 'last_success' => $this->error_stats['last_success']
+ ]);
+ }
$this->ensureInitialized();
if (!$this->isSetUp()){
$this->logError('Connection not setup for '.$this->service_name, [
@@ -1920,7 +1926,6 @@
return false;
}
- // Build refresh request data
$request_data = [
'client_id' => $this->credentials['client_id'],
'client_secret' => $this->credentials['client_secret'],
@@ -1928,12 +1933,24 @@
'grant_type' => 'refresh_token'
];
- // Use centralized OAuth request method
$response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
if (is_wp_error($response)) {
+ $error_message = $response->get_error_message();
+
+ if (str_contains($error_message, 'invalid_grant')) {
+ $this->logError('OAuth refresh token is invalid - user must re-authorize', [
+ 'error' => $error_message
+ ], 'critical');
+
+ // Mark unhealthy immediately
+ $this->error_stats['consecutive_errors'] = $this->error_threshold;
+ $this->is_healthy = false;
+ $this->saveErrorStats();
+ }
+
$this->logError('Failed to refresh OAuth token for '.$this->service_name, [
- 'error' => $response->get_error_message()
+ 'error' => $error_message
]);
return false;
}
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index a8b7a43..6f81f6f 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -5,28 +5,36 @@
exit;
}
+use JVBase\utility\Features;
use WP_Block;
use WP_Query;
class DirectoryManager
{
protected array $directories;
+ protected array $directoryPageIDs;
+ protected array $directoryList;
protected static string $type = BASE.'for_type';
protected static string $slug = BASE.'for_type_slug';
protected CacheManager $cache;
public function __construct()
{
- $this->directories = jvbGlobalDirectoryInfo();
- if (empty(jvbGlobalDirectories())) {
+ $this->directories = $this->getDirectories();
+ if (empty($this->directories)) {
return;
}
$this->cache = CacheManager::for('directory', WEEK_IN_SECONDS);
+ foreach(['content','taxonomy','user'] as $key) {
+ if (array_key_exists($key, $this->directories)) {
+ $this->cache->connectTo($key);
+ }
+ }
add_action('init', [$this, 'registerDirectories']);
jvb_register_do_once('directories_registered', [$this, 'activate']);
- add_action('render_block', [$this, 'renderBlock'], 99, 3);
+ add_action('render_block', [$this, 'renderBlock'], 99999, 3);
}
public function registerDirectories()
@@ -62,81 +70,121 @@
));
}
+ public function getDirectories():array
+ {
+ $directories = get_option(BASE.'directories');
+ if (!$directories) {
+ $directories = [];
+ //content
+ if(Features::anyContentHas('show_directory')) {
+ foreach (JVB_CONTENT as $key => $config) {
+ if (Features::forContent($key)->has('show_directory')) {
+ $directories[$key] = 'content';
+ }
+ }
+ }
+ if(Features::anyTaxonomyHas('show_directory')) {
+ foreach (JVB_TAXONOMY as $key=>$config) {
+ if (Features::forTaxonomy($key)->has('show_directory')) {
+ $directories[$key] = 'taxonomy';
+ }
+ }
+ }
+ if (Features::anyUserHas('show_directory')) {
+ foreach(JVB_USER as $key=>$config) {
+ if (Features::forUser($key)->has('show_directory')) {
+ $directories[$key] = 'user';
+ }
+ }
+ }
+
+ update_option(BASE.'directories', $directories);
+ }
+ return $directories;
+ }
+ protected function getConfigFromType(string $type):array
+ {
+ if (!array_key_exists($type, $this->directories)) {
+ return [];
+ }
+ return match ($this->directories[$type]) {
+ 'content' => JVB_CONTENT[$type],
+ 'taxonomy' => JVB_TAXONOMY[$type],
+ 'user' => JVB_USER[$type],
+ default => [],
+ };
+
+ }
+
public function activate()
{
$created = [];
$directories = [];
- foreach (jvbGlobalDirectories() as $directory => $type) {
- switch ($type) {
- case 'content':
- $config = JVB_CONTENT;
- break;
- case 'tax':
- $config = JVB_TAXONOMY;
- break;
- case 'user':
- $config = JVB_USER;
- break;
- }
- $title = $config[$directory]['directory']??$config[$directory]['plural'];
- $excerpt = implode(' ', $config[$directory]['description']??[]);
- $ID = wp_insert_post([
- 'post_type' => BASE.'directory',
- 'post_title'=> $title,
- 'post_status'=> 'publish',
- 'post_excerpt' => $excerpt,
- 'slug' => sanitize_title($title)
- ]);
- if (!is_wp_error($ID)) {
- add_post_meta($ID, self::$type, $type);
- add_post_meta($ID, self::$slug, $directory);
- $created[$directory] = (int)$ID;
- $slug = sanitize_title($title);
- $directories[$directory] = [
- 'slug' => $slug,
- 'title' => $title,
- 'ID' => $ID,
- 'url' => get_home_url(null, '/directory/'.$slug),
- 'page' => $title,
- 'description' =>$config[$directory]['description']??[],
- 'type' => $type,
- 'extra' => $config[$directory]['directory_extra'] ??[],
- ];
- }
+ foreach($this->directories as $directory => $type) {
+ $config = $this->getConfigFromType($directory);
+ $title = $config['directory']??$config['plural'];
+ $excerpt = implode(' ', $config['description']??[]);
+ $ID = wp_insert_post([
+ 'post_type' => BASE.'directory',
+ 'post_title' => $title,
+ 'post_status' => 'publish',
+ 'post_excerpt' => $excerpt,
+ 'slug' => sanitize_title($title)
+ ]);
+ if (!is_wp_error($ID)) {
+ add_post_meta($ID, self::$type, $type);
+ add_post_meta($ID, self::$slug, $directory);
+ $created[$directory] = (int)$ID;
+ $slug = sanitize_title($title);
+ $directories[$directory] = [
+ 'slug' => $slug,
+ 'title' => $title,
+ 'ID' => $ID,
+ 'url' => get_home_url(null, '/directory/'.$slug),
+ 'page' => $title,
+ 'description' =>$config[$directory]['description']??[],
+ 'type' => $type,
+ 'extra' => $config[$directory]['directory_extra'] ??[],
+ ];
+ }
+ $isGrouped = match ($type) {
+ 'content' => Features::forContent($directory)->has('isGrouped'),
+ 'taxonomy' => Features::forTaxonomy($directory)->has('isGrouped'),
+ 'user' => Features::forUser($directory)->has('isGrouped'),
+ default => false,
+ };
+ if ($isGrouped) {
+ $title = $title.', but Grouped';
+ $slug = sanitize_title($title).'-grouped';
+ $excerpt = $config['groupedDescription']??'Too many options? This is grouped by type.';
+ $ID = wp_insert_post([
+ 'post_type' => BASE.'directory',
+ 'post_title' => $title,
+ 'post_status' => 'publish',
+ 'post_excerpt' => $excerpt,
+ 'slug' => $slug,
+ ]);
+ if (!is_wp_error($ID)) {
+ add_post_meta($ID, self::$type, $type);
+ add_post_meta($ID, self::$slug, $directory.'-grouped');
+ add_post_meta($ID, BASE.'grouped_directory', 'yup');
+ $created[$directory.'-grouped'] = (int)$ID;
+ $directories[$directory.'-grouped'] = [
+ 'slug' => $slug,
+ 'title' => $title,
+ 'ID' => $ID,
+ 'url' => get_home_url(null, '/directory/'.$slug),
+ 'page' => $title,
+ 'description' =>$config[$directory]['description']??[],
+ 'type' => $type,
+ 'extra' => $config[$directory]['directory_extra'] ??[],
+ ];
+ }
+ }
+ }
-
- if (jvbCheck('isGrouped', $config[$directory])) {
- $title = $title. ', but Grouped';
- $excerpt = 'Too many options in the list? This is grouped by type, nested deep, man.';
- $slug = sanitize_title(str_replace(', but', '', $title));
- $ID = wp_insert_post([
- 'post_type' => BASE.'directory',
- 'post_title'=> $title,
- 'post_status'=> 'publish',
- 'post_excerpt' => $excerpt,
- 'slug' => $slug
- ]);
- if (!is_wp_error($ID)) {
- add_post_meta($ID, self::$type, $type);
- add_post_meta($ID, self::$slug, $directory.'-grouped');
- add_post_meta($ID, BASE.'grouped_directory', 'yup');
- $created[$directory.'-grouped'] = (int)$ID;
- $directories[$directory.'-grouped'] = [
- 'slug' => $slug,
- 'title' => $title,
- 'ID' => $ID,
- 'url' => get_home_url(null, '/directory/'.$slug),
- 'page' => $title,
- 'description' =>$config[$directory]['description']??[],
- 'type' => $type,
- 'extra' => $config[$directory]['directory_extra'] ??[],
- ];
- }
- }
-
- }
- if (jvbCheck('has_map', JVB_SITE)) {
+ if (Features::forSite()->has('has_map')) {
$ID = wp_insert_post([
'post_type' => BASE.'directory',
'post_title' => 'Map',
@@ -165,20 +213,32 @@
}
}
+ public function getDirectoryPageIDs():array
+ {
+ if (empty($this->directoryPageIDs)) {
+ $this->directoryPageIDs = get_option(BASE.'directory_ids', []);
+ }
+ return $this->directoryPageIDs;
+ }
+ public function getDirectoryList():array
+ {
+ if (empty($this->directoryList)) {
+ $this->directoryList = get_option(BASE.'directory_list', []);
+ }
+ return $this->directoryList;
+ }
+
public static function getConfig(int $ID):array
{
$type = get_post_meta($ID, self::$type, true);
$slug = get_post_meta($ID, self::$slug, true);
- switch ($type) {
- case 'content':
- return JVB_CONTENT[$slug];
- case 'taxonomy':
- return JVB_TAXONOMY[$slug];
- case 'user':
- return JVB_USER[$slug];
- }
- return [];
- }
+ return match ($type) {
+ 'content' => JVB_CONTENT[$slug],
+ 'taxonomy' => JVB_TAXONOMY[$slug],
+ 'user' => JVB_USER[$slug],
+ default => [],
+ };
+ }
public function letters():array
{
@@ -212,7 +272,7 @@
];
}
- protected function alphabetizeMe(
+ public function alphabetizeMe(
array $list,
string $name = '',
string $url = '',
@@ -239,19 +299,35 @@
return $list;
}
+ public function directories(string $search = 'all'):array
+ {
+ $directories = $this->getDirectories();
+ if ($search === 'all') {
+ return $directories;
+ }
+ return $directories[$search]??[];
+ }
+
+ public function isDirectory():bool
+ {
+ return (is_post_type_archive(BASE.'directory') || is_singular(BASE.'directory'));
+ }
+
private function renderArchive(): string
{
+ $this->getDirectoryList();
return $this->cache->remember(
'archive',
function() {
$cache = '<h1>Directory of Directories</h1>
<p>You like lists? We\'ve got \'em!</p>
<section class="directories item-grid">';
- foreach ($this->directories as $slug => $directory) {
+ foreach ($this->directoryList as $slug => $directory) {
+ $config = $this->getConfigFromType($slug);
$aOpen = '<a href="'.$directory['url'].'" title="See our list of '.$directory['title'].'">';
$aClose = '</a>';
$cache .= '<div class="directory col start">
- '.$aOpen.jvbIcon($slug).$aClose.
+ '.$aOpen.jvbIcon($config['icon']).$aClose.
'<h2>'.$aOpen.$directory['title'].$aClose.'</h2>';
if (!empty($directory['description'])) {
$cache .= '<div class="description">';
@@ -273,11 +349,14 @@
$cache = $this->cache->remember(
'index',
function() {
- $cache = '<nav class="directory"><ul>';
- foreach (jvbDirectories() as $slug => $directory) {
+ $cache = '<nav class="directory condensed"><ul>';
+ foreach ($this->getDirectoryList() as $slug => $directory) {
+ $actualSlug = str_replace('-grouped', '', $slug);
+ $config = $this->getConfigFromType($actualSlug);
+ $icon = jvbIcon($config['icon']??'');
$cache .= '<li id="'.$slug.'">
- <a href="'.$directory['url'].'">'.
- jvbIcon(str_replace('-grouped', '', $slug)).$directory['title'].'
+ <a href="'.$directory['url'].'" class="'.$actualSlug.'">'.
+ $icon.$directory['title'].'
</a>
</li>';
}
@@ -285,7 +364,7 @@
return $cache;
}
);
- if ($current !== '' && array_key_exists($current, jvbDirectories())) {
+ if ($current !== '' && array_key_exists($current, $this->directories())) {
$open = ($open) ? ' open' : '';
$cache = '<details'.$open.'><summary class="row btw">Other Directories:</summary>'.
str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache)
@@ -301,21 +380,23 @@
if ($slug === '') {
return '';
}
+ $this->directories();
return $this->cache->remember(
$slug,
function() use ($slug) {
- $out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
+ $config = $this->getConfigFromType($slug);
+ $out = '<h1>'.$config['directory'].'</h1>';
$out .= '<div class="description">';
- foreach ($this->directories[$slug]['description']??[] as $p) {
+ foreach ($config[$slug]['description']??[] as $p) {
$out .= '<p>'.$p.'</p>';
}
$out .= '</div>';
$out .= $this->renderIndex($slug);
- $data = $this->directories[$slug];
+ $type = $this->directories[$slug];
$list = [];
- switch ($data['type']) {
+ switch ($type) {
case 'content':
$get = new WP_Query([
'post_type' => jvbCheckBase($slug),
@@ -324,37 +405,40 @@
'order' => 'ASC'
]);
+ $hasExtra = Features::forContent($slug)->has('directory_extra');
if ($get->have_posts()) {
while ( $get->have_posts() ) {
$get->the_post();
$extra = [];
- foreach ( $data['extra'] as $item ) {
- $item = jvbCheckBase( $item );
+ if ($hasExtra) {
+ foreach ($config['directory_extra'] as $item ) {
+ $item = jvbCheckBase( $item );
- $terms = get_the_terms( get_the_ID(), jvbCheckBase( $item ) );
- if ( $terms && ! is_wp_error( $terms ) ) {
- $term = $terms[0];
- $extra[] = [
- 'name' => (get_term_meta( $term->term_id, BASE . 'singular', true ) !== '') ? get_term_meta( $term->term_id, BASE . 'singular', true ) : $term->name,
- 'url' => get_term_link( $term->term_id, $item ),
- 'id' => $term->term_id,
- 'type' => $item,
- ];
+ $terms = get_the_terms( get_the_ID(), jvbCheckBase( $item ) );
+ if ( $terms && ! is_wp_error( $terms ) ) {
+ $term = $terms[0];
+ $extra[] = [
+ 'name' => (get_term_meta( $term->term_id, BASE . 'singular', true ) !== '') ? get_term_meta( $term->term_id, BASE . 'singular', true ) : $term->name,
+ 'url' => get_term_link( $term->term_id, $item ),
+ 'id' => $term->term_id,
+ 'type' => $item,
+ ];
+ }
}
- $list = $this->alphabetizeMe(
- $list,
- get_the_title(),
- get_the_permalink(),
- get_the_ID(),
- $extra
- );
}
+ $list = $this->alphabetizeMe(
+ $list,
+ get_the_title(),
+ get_the_permalink(),
+ get_the_ID(),
+ $extra
+ );
}
}
wp_reset_postdata();
break;
- case 'tax':
+ case 'taxonomy':
$get = get_terms([
'taxonomy' => jvbCheckBase($slug),
'hide_empty' => true,
@@ -363,7 +447,7 @@
]);
if ($get && !is_wp_error($get)) {
- $extra = false;
+ $extra = [];
foreach ($get as $term) {
$list = $this->alphabetizeMe(
@@ -384,9 +468,6 @@
]);
break;
- default:
- $list = [];
- break;
}
$out .= '<section class="directory-list '.$slug.'">';
@@ -412,7 +493,7 @@
}
return $this->cache->remember(
$slug.'_group',
- function() {
+ function() use ($slug){
$out = '<h1>'.$this->directories[$slug]['title'].'</h1>';
$out .= '<div class="description">';
@@ -493,7 +574,7 @@
$out = '<ul class="list-none">';
foreach ($list as $letter => $items) {
- $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw"><h3>'.strtoupper($letter).'</h3><ul>';
+ $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
foreach ($items as $item) {
$extra = '';
if (!empty($item['extra'])) {
@@ -525,15 +606,13 @@
return $content;
}
-
// For archive page
if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') {
- return ($block['attrs']['tagName']??false === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
+ return ($block['attrs']['tagName']??'' === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
}
// For single directory posts
if ($block['blockName'] === 'core/group') {
-
switch (get_post_meta(get_the_ID(), BASE.'grouped_directory', true)) {
case '':
return '<main>' . $this->renderDirectory() . '</main>';
@@ -545,15 +624,3 @@
return $content;
}
}
-
-new DirectoryManager();
-
-function jvbDirectoryConfig():array
-{
- $ID = get_the_ID();
- if ($ID) {
- return DirectoryManager::getConfig($ID);
- }
- return [];
-}
-
diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index 383e687..d244340 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -43,7 +43,6 @@
{
$this->source = $source;
$this->cache = CacheManager::for('icons_' . $source, WEEK_IN_SECONDS);
-
$this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
? JVB_SITE['icons']
: 'regular';
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 538a5e8..32e91a6 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -24,6 +24,7 @@
private function __construct()
{
$this->cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all');
+// $this->cache->clear();
}
public static function getInstance(): self
@@ -93,7 +94,8 @@
$crumbs = $this->addTaxonomyCrumbs($crumbs, $obj);
} elseif (is_singular()) {
$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
- $crumbs = $this->addSingularCrumbs($crumbs, $obj);
+ $hierarchy = $this->addSingularCrumbs($crumbs, $obj);
+ $crumbs = $crumbs + $hierarchy;
} elseif (is_post_type_archive() && !is_post_type_archive(BASE.'dash')) {
$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
}
@@ -124,7 +126,7 @@
// Add directory if exists
if (Features::forTaxonomy($tax)->has('directory')) {
- $directory = jvbDirectories($tax);
+ $directory = JVB()->directories()?->directories($tax);
$crumbs[] = [
'name' => $directory['title'],
'url' => $directory['url']
@@ -143,23 +145,26 @@
private function addSingularCrumbs(array $crumbs, WP_Post $post): array
{
// Add directory if exists
- $directory = jvbDirectories(jvbNoBase($post->post_type));
- if (!empty($directory)) {
- $crumbs[] = [
- 'name' => $directory['title'],
- 'url' => $directory['url']
- ];
+ $content = jvbNoBase($post->post_type);
+ if(Features::forContent($content)->has('show_directory')) {
+ $directory = JVB()->directories()->getDirectoryList()[$content]??[];
+ if (!empty($directory)) {
+ $crumbs[] = [
+ 'name' => $directory['title'],
+ 'url' =>$directory['url']
+ ];
+ }
}
// Handle directory posts specially
- if (jvbIsDirectory()) {
+ if (JVB()->directories()->isDirectory()) {
$pos = jvbGetDirectoryInfo();
if (!empty($pos)) {
// Special case for map
if ($pos['title'] == 'Map') {
$crumbs[] = [
'name' => 'Tattoo Shops',
- 'url' => jvbDirectories(BASE.'shop')['url']
+ 'url' => JVB()->directories()?->directories(BASE.'shop')['url']
];
}
@@ -169,6 +174,10 @@
];
}
} else {
+ $name = jvbNoBase($post->post_type);
+ if (Features::forContent($name)->has('addCrumb')) {
+ $this->addTaxToCrumbs($crumbs, JVB_CONTENT[$name]['addCrumb']);
+ }
// Add post hierarchy
$crumbs = array_merge($crumbs, $this->buildPostHierarchy($post));
}
@@ -183,7 +192,13 @@
{
$type = is_singular() ? $obj->post_type : $obj->name;
$name = jvbNoBase($type);
- if (array_key_exists($name, JVB_CONTENT)) {
+
+ if (Features::forSite()->has('is_directory') && $name === 'directory') {
+ $crumbs[] = [
+ 'name' => 'Directory',
+ 'url' => get_post_type_archive_link($type)
+ ];
+ } elseif ((is_post_type_archive() || !Features::forContent($name)->has('show_directory')) && array_key_exists($name, JVB_CONTENT)) {
$crumbs[] = [
'name' => JVB_CONTENT[$name]['breadcrumb'] ?? JVB_CONTENT[$name]['plural'],
'url' => get_post_type_archive_link($type)
@@ -339,4 +354,30 @@
$this->cache->clear();
}
}
+
+ public function addTaxToCrumbs(array $crumbs, string $taxonomy):array
+ {
+ $ID = get_the_ID();
+ $taxonomy = jvbCheckBase($taxonomy);
+ $terms = get_the_terms($ID, $taxonomy);
+ if ($terms && !is_wp_error($terms)) {
+ $term = $terms[0];
+ $ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy');
+ $ancestors = array_reverse($ancestors);
+ foreach ($ancestors as $ancestor) {
+ $aTerm = get_term($ancestor, $taxonomy);
+ if ($aTerm && !is_wp_error($aTerm)) {
+ $crumbs[] = [
+ 'name' => $aTerm->name,
+ 'url' => get_term_link($ancestor, $taxonomy)
+ ];
+ }
+ }
+ $crumbs[] = [
+ 'name' => $term->name,
+ 'url' => get_term_link($term, $taxonomy)
+ ];
+ }
+ return $crumbs;
+ }
}
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index e86780a..f59cdfd 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -343,6 +343,7 @@
);
+
//Notifications
wp_register_script(
'jvb-notifications',
diff --git a/inc/meta/MetaForm.php b/inc/meta/MetaForm.php
index 733e141..aa537c0 100644
--- a/inc/meta/MetaForm.php
+++ b/inc/meta/MetaForm.php
@@ -818,7 +818,7 @@
//Processing Options
'max_size' => null, // Override default size limits
'convert' => 'webp', // Image conversion format
- 'quality' => 80, // Conversion quality
+ 'quality' => 90, // Conversion quality
'create_thumbnails' => true,
];
$config = array_merge($defaultConfig, $field);
@@ -917,6 +917,7 @@
<?php endif; ?>
<div class="file-error"></div>
</div>
+ <?php jvbRenderProgressBar(); ?>
</div>
@@ -927,7 +928,7 @@
<div class="selection-controls">
<div class="selected">
<div class="field">
- <input type="checkbox" id="select-all-uploads" name="select-all-uploads">
+ <input type="checkbox" id="select-all-uploads" data-select-all data-selects="item-grid" name="select-all-uploads">
<label for="select-all-uploads">
Select All
</label>
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 95745c4..118111c 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -44,7 +44,25 @@
$this->userIDType = $this->getColumnType($this->userTable, 'ID');
$this->termIDType = $this->getColumnType($this->wpdb->terms, 'term_id');
$this->postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
+
+ error_log("JVB DEBUG: userTable = " . $this->userTable);
+ error_log("JVB DEBUG: Users table exists = " . ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->userTable}'") ? 'yes' : 'no'));
+ error_log("JVB DEBUG: Users engine = " . $this->wpdb->get_var("SELECT ENGINE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}'"));
+ error_log("JVB DEBUG: MySQL version = " . $this->wpdb->db_version());
+ $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->userTable}'");
+ $termsStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->terms}'");
+ error_log("JVB DEBUG: wp_users collation = " . $usersStatus->Collation);
+ error_log("JVB DEBUG: wp_terms collation = " . $termsStatus->Collation);
+ error_log("JVB DEBUG: Our charset_collate = " . $this->wpdb->get_charset_collate());
+
error_log("JVB FK Types: users.ID={$this->userIDType}, terms.term_id={$this->termIDType}, posts.ID={$this->postIDType}");
+
+ $usersDb = $this->wpdb->get_var(
+ "SELECT TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->userTable}' LIMIT 1"
+ );
+ error_log("JVB DEBUG: wp_users is in database = " . $usersDb);
+ error_log("JVB DEBUG: Current DB_NAME = " . DB_NAME);
+ error_log("JVB DEBUG: Tables being created in = " . $this->wpdb->dbname);
}
protected function getMultisiteUsersTable():string
@@ -368,73 +386,70 @@
}
}
- public function createTables(array $tables)
- {
- $charset_collate = $this->wpdb->get_charset_collate();
- require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ public function createTables(array $tables)
+ {
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
- $errors = [];
- $created = [];
+ // Match collation from existing WP tables for FK compatibility
+ $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'");
+ $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci';
+ $charset_collate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}";
- foreach ($tables as $name => $schema) {
- $table_name = $this->prefix . BASE . $name;
- $full_schema = "CREATE TABLE IF NOT EXISTS {$table_name} {$schema} {$charset_collate};";
+ error_log("JVB: Using charset_collate: " . $charset_collate);
- // Clear any previous errors
- $this->wpdb->flush();
+ $errors = [];
+ $created = [];
- try {
- $result = dbDelta($full_schema);
+ foreach ($tables as $name => $schema) {
+ $table_name = $this->prefix . BASE . $name;
- // Check for SQL errors
- if ($this->wpdb->last_error) {
- $error_msg = "SQL Error creating table {$table_name}: " . $this->wpdb->last_error;
- error_log($error_msg);
- error_log("Failed SQL Query: " . $full_schema);
- $errors[] = $error_msg;
+ // Skip if exists
+ if ($this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'")) {
+ $created[] = $table_name . ' (exists)';
+ continue;
+ }
- // Don't throw exception, just log and continue
- continue;
- }
+ $full_schema = "CREATE TABLE IF NOT EXISTS {$table_name} {$schema} {$charset_collate}";
- // Verify table was actually created
- $table_exists = $this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'");
- if ($table_exists) {
- $created[] = $table_name;
- error_log("Successfully created table: {$table_name}");
- } else {
- $error_msg = "Table {$table_name} was not created (no error reported)";
- error_log($error_msg);
- error_log("Schema used: " . $full_schema);
- $errors[] = $error_msg;
- }
+ $this->wpdb->flush();
- } catch (Exception $e) {
- $error_msg = "Exception creating table {$table_name}: " . $e->getMessage();
- error_log($error_msg);
- error_log("Exception SQL Query: " . $full_schema);
- $errors[] = $error_msg;
- }
- }
+ // Use direct query - dbDelta mangles FK constraints
+ $hasForeignKey = stripos($schema, 'FOREIGN KEY') !== false;
- // Log summary
- if (!empty($created)) {
- error_log("JVB Tables Created Successfully: " . implode(', ', $created));
- }
+ if ($hasForeignKey) {
+ $result = $this->wpdb->query($full_schema);
+ $success = ($result !== false && !$this->wpdb->last_error);
+ } else {
+ dbDelta($full_schema . ';');
+ $success = !$this->wpdb->last_error;
+ }
- if (!empty($errors)) {
- error_log("JVB Table Creation Errors (" . count($errors) . " total):");
- foreach ($errors as $error) {
- error_log(" - " . $error);
- }
+ if (!$success) {
+ $error_msg = "SQL Error creating table {$table_name}: " . $this->wpdb->last_error;
+ error_log($error_msg);
+ error_log("Failed SQL Query: " . $full_schema);
+ $errors[] = $error_msg;
+ } elseif ($this->wpdb->get_var("SHOW TABLES LIKE '{$table_name}'")) {
+ $created[] = $table_name;
+ error_log("Successfully created table: {$table_name}");
+ }
+ }
- // Optionally store errors for admin display
- update_option(BASE . 'table_creation_errors', $errors);
- } else {
- // Clear any previous errors
- delete_option(BASE . 'table_creation_errors');
- }
- }
+ // Log summary
+ if (!empty($created)) {
+ error_log("JVB Tables Created Successfully: " . implode(', ', $created));
+ }
+
+ if (!empty($errors)) {
+ error_log("JVB Table Creation Errors (" . count($errors) . " total):");
+ foreach ($errors as $error) {
+ error_log(" - " . $error);
+ }
+ update_option(BASE . 'table_creation_errors', $errors);
+ } else {
+ delete_option(BASE . 'table_creation_errors');
+ }
+ }
/******************************************************
* Table Definitions
@@ -575,9 +590,9 @@
KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`),
KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`),
CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
@@ -617,7 +632,7 @@
UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`),
KEY `user_status` (`user_id`, `status`),
CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`)
REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE
)",
@@ -637,7 +652,7 @@
KEY `user_frequency` (`user_id`, `frequency`),
KEY `frequency_lookup` (`frequency`, `last_sent`),
CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
// Notification digest scheduling and tracking
@@ -654,7 +669,7 @@
KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`),
KEY `user_digests` (`user_id`, `frequency`),
CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
// Analytics on notification interactions
@@ -671,9 +686,9 @@
KEY `user_actions` (`user_id`, `action`),
KEY `action_analysis` (`action`, `action_source`),
CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`)
- REFERENCES {$this->prefixed}notifications (`id`) ON DELETE CASCADE
+ REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE
)"
];
}
@@ -702,7 +717,7 @@
KEY `status` (`status`),
KEY `expiring_requests` (`status`, `expires_at`),
CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
$tables['approval_'.$type.'_votes'] = "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -715,9 +730,9 @@
UNIQUE KEY `unique_vote` (`request_id`, `user_id`),
KEY `user_votes` (`user_id`),
CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
- REFERENCES {$this->prefixed}approval_{$type}_requests (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
}
if (!empty($save)) {
@@ -747,9 +762,9 @@
KEY `related_taxonomy` (`related_taxonomy`),
UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`),
CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
)"
];
@@ -767,9 +782,9 @@
KEY `user_taxonomy` (`user_id`, `taxonomy`),
KEY `taxonomy` (`taxonomy`),
CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
)";
}
@@ -792,7 +807,7 @@
KEY `user_type` (`user_id`, `type`),
KEY `target_type` (`target_id`, `type`),
CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
'favourites_lists' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -804,7 +819,7 @@
PRIMARY KEY (`id`),
KEY `user_lists` (`user_id`),
CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
'favourites_list_items' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -818,9 +833,9 @@
KEY `list_items` (`list_id`),
KEY `favourite_id` (`favourite_id`),
CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`)
- REFERENCES {$this->prefixed}favourites_lists (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`)
- REFERENCES {$this->prefixed}favourites (`id`) ON DELETE SET NULL
+ REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL
)",
'favourites_list_shares' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -838,9 +853,9 @@
KEY `list_shares` (`list_id`),
KEY `status_index` (`status`),
CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`)
- REFERENCES {$this->prefixed}favourites_lists (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
'favourites_list_stats' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -872,11 +887,11 @@
KEY `user_id` (`user_id`),
KEY `artist_id` (`artist_id`),
CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`)
- REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE SET NULL
+ REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL
)"
];
}
@@ -904,9 +919,9 @@
KEY `parent_child` (`parent_id`),
KEY `karma_order` (`karma`),
CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`)
- REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`)
- REFERENCES {$this->prefixed}responses (`id`) ON DELETE SET NULL
+ REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL
)",
'karma_response' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -919,9 +934,9 @@
KEY `item_id` (`item_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`)
- REFERENCES {$this->prefixed}responses (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)"
];
}
@@ -972,9 +987,9 @@
KEY `item_id` (`item_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`)
- REFERENCES {$reference_table} ({$reference_column}) ON DELETE CASCADE,
+ REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
}
@@ -1051,13 +1066,13 @@
KEY `rsvp_events` (`rsvp_required`, `rsvp_deadline`),
CONSTRAINT `{$this->base}cal_{$type}_type` FOREIGN KEY (`event_type`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE,
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}cal_{$type}_post` FOREIGN KEY (`post_id`)
- REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}cal_{$type}_shop` FOREIGN KEY (`shop_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE SET NULL,
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE SET NULL,
CONSTRAINT `{$this->base}cal_{$type}_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE SET NULL
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE SET NULL
)";
$tables['calendar_'.$type.'_participants'] = "(
@@ -1069,9 +1084,9 @@
PRIMARY KEY (`id`),
UNIQUE KEY `event_user` (`event_id`, `user_id`),
CONSTRAINT `{$this->base}cal_{$type}_participant_event` FOREIGN KEY (`event_id`)
- REFERENCES {$this->prefixed}calendar_{$type} (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}calendar_{$type}` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}cal_{$type}_participant_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
$tables['calendar_'.$type.'_recurrence_exceptions'] = "(
@@ -1117,9 +1132,9 @@
KEY `user_idx` (`user_id`),
KEY `owner_idx` (`owner_id`),
CONSTRAINT `{$this->base}umami_user_id_link` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}umami_owner_id_link` FOREIGN KEY (`owner_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)",
'stats_performance' => "(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
@@ -1137,7 +1152,7 @@
PRIMARY KEY (`id`),
KEY `user_date_idx` (`user_id`, `date`),
CONSTRAINT `{$this->base}performance_user_id_link` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)"
];
}
@@ -1170,7 +1185,7 @@
)";
foreach($config['to_terms']??[] as $term) {
$definitions .= "CONSTRAINT `{$this->base}_{$term}_link` FOREIGN KEY (`to_{$term}`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE";
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE";
}
$tables['invitations_'.$role] = $definitions;
@@ -1199,9 +1214,9 @@
UNIQUE KEY `content_term` (`content_id`, `term_id`),
KEY content_role (`term_id`, `role`),
CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
)";
}
}
@@ -1228,9 +1243,9 @@
PRIMARY KEY (`id`),
UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`),
CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
)";
}
}
@@ -1271,9 +1286,9 @@
KEY `date_idx` (`referred_at`),
KEY `consult_idx` (`consulted_at`),
CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE,
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
// Create the main referrals table first
@@ -1297,7 +1312,7 @@
KEY `user_idx` (`user_id`),
KEY `email_idx` (`email`),
CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
// Third: referral_treatments (depends on referrals AND wp_users)
@@ -1317,9 +1332,9 @@
KEY `date_idx` (`treatment_date`),
KEY `type_idx` (`treatment_type`),
CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`)
- REFERENCES {$this->prefixed}referrals (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
// Fourth: referral_rewards (depends on referrals AND wp_users)
@@ -1341,9 +1356,9 @@
KEY `status_idx` (`status`),
KEY `type_idx` (`reward_type`),
CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`)
- REFERENCES {$this->prefixed}referrals (`id`) ON DELETE CASCADE,
+ REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
)";
return $dependentTables;
@@ -1465,13 +1480,13 @@
if ($type === 'taxonomy') {
$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
- REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE SET NULL";
+ REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE SET NULL";
} elseif ($type === 'user') {
$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE SET NULL";
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE SET NULL";
} elseif ($type === 'image' || $type === 'file') {
$constraints[] = "CONSTRAINT `{$constraintName}` FOREIGN KEY (`{$fieldName}`)
- REFERENCES {$this->wpdb->posts} (`ID`) ON DELETE SET NULL";
+ REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL";
}
}
@@ -1568,7 +1583,7 @@
["PRIMARY KEY (`term_id`)"],
$indexes,
$constraints,
- ["CONSTRAINT `{$base}_{$type}_term` FOREIGN KEY (`term_id`) REFERENCES {$this->wpdb->terms} (`term_id`) ON DELETE CASCADE"]
+ ["CONSTRAINT `{$base}_{$type}_term` FOREIGN KEY (`term_id`) REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE"]
);
$sql = "(\n " . implode(",\n ", $allDefinitions) . "\n)";
error_log("JVB: Creating content table for type: {$type}");
@@ -1732,7 +1747,7 @@
$constraints = [
"CONSTRAINT `{$this->base}_{$userType}_stats_user` FOREIGN KEY (`user_id`)
- REFERENCES {$this->userTable} (`ID`) ON DELETE CASCADE"
+ REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE"
];
$allDefinitions = array_merge($columns, $indexes, $constraints);
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index e323a70..468a1a7 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -36,7 +36,7 @@
{
$this->cache_name = 'user_content_'.get_current_user_id();
parent::__construct();
-
+ $this->cache->clear();
$this->action = 'dash-';
$this->operation_type = 'content_update';
add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -844,6 +844,7 @@
$data['images'] = $images;
}
+ error_log('Got Data for post: '.print_r($data, true));
return $data;
}
protected function extractImages(array $fields = []):array
diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index f871502..92375c2 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -32,17 +32,17 @@
$this->cache_name = 'feed';
$this->cache_ttl = 86400;
parent::__construct();
+ $this->cache->clear();
}
public function init():void
{
+ $this->cache->clear();
$this->checker = Checker::getInstance();
if (jvbSiteUsesUmami()) {
$this->tracker = JVB()->connect('umami');
}
-
- $this->cache->clear();
$this->setupCacheConnections();
}
@@ -111,9 +111,9 @@
if (!$post || is_wp_error($post)) {
return [];
}
-
- return $cache->remember($postID,
- function() use ($postID, $type, $metaType, $post, $skip) {
+//
+// return $cache->remember($postID,
+// function() use ($postID, $type, $metaType, $post, $skip) {
$config = null;
switch ($metaType) {
case 'post':
@@ -150,10 +150,10 @@
return $field['type'] === 'taxonomy';
});
foreach ($temp as $key => $config) {
- if (array_key_exists($key, $out) && $out[$key] !== '') {
- $IDs = array_map('absint', explode(',', $out[$key]));
+ if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
+ $IDs = array_map('absint', explode(',', $out['fields'][$key]));
$data = [];
- $icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??'triangle';
+ $icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??jvbDefaultIcon();
foreach ($IDs as $ID) {
$term = get_term($ID, jvbCheckBase($config['taxonomy']));
if ($term && !is_wp_error($term)) {
@@ -179,9 +179,10 @@
$temp = array_filter($fields, function($field) {
return in_array($field['type'], [ 'upload', 'image', 'gallery']);
});
+
foreach ($temp as $key => $config) {
- if (array_key_exists($key, $out) && $out[$key] !== '') {
- $IDs = array_map('absint', explode(',',$out[$key]));
+ if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
+ $IDs = array_map('absint', explode(',',$out['fields'][$key]));
foreach ($IDs as $ID) {
$imgIDs[$ID] = jvbImageData($ID);
}
@@ -191,7 +192,7 @@
$out['id'] = $postID;
$out['content'] = $type;
- $out['icon'] = $config['icon']??'triangle';
+ $out['icon'] = $config['icon']??jvbDefaultIcon();
if ($this->tracker) {
$args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
@@ -214,12 +215,14 @@
$out['url'] = get_the_permalink($postID);
break;
}
- return $out;
- }
- );
-
+// return $out;
+// }
+// );
+ return $out;
}
+
+
protected function initTimelineFields(string $content):void
{
$content = jvbNoBase($content);
@@ -336,7 +339,6 @@
protected function buildRequestArgs(WP_REST_Request $request): array
{
$data = $request->get_params();
- error_log('Feed Request: ' . print_r($data, true));
$args = [
'post_type' => (array_key_exists($data['content'], $this->buildFeedTypesConfig())) ?
BASE . $data['content'] :
@@ -362,7 +364,6 @@
$args = $this->applyDateFilters($args, $data);
$args = $this->applyFavouritesFilter($args, $data);
- error_log('Final Args: '.print_r($args, true));
return $args;
}
@@ -438,9 +439,6 @@
// Fetch and format items
$items = $this->fetchFeedItems($args);
- error_log('Feed Got items: ' .print_r($items, true));
-
-
$ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
$this->cache->set($key, $items, $ttl);
@@ -536,7 +534,6 @@
*/
protected function processHighlightedItem(array $items, array $data): array
{
- error_log('Data passed to processHighlightedItem:' . print_r($data, true));
if (empty($data['highlight'] ?? null)) {
return $items;
}
@@ -627,7 +624,6 @@
if (!array_key_exists('favourites', $filters)) {
return $args;
}
- error_log('Proceeding to check for favourites:');
global $wpdb;
// Get post types for the current filter
@@ -637,7 +633,6 @@
$favourites_table = $wpdb->prefix . BASE . 'favourites';
$placeholders = implode(',', array_fill(0, count($post_types), '%s'));
- error_log('CurrentUser ID: ' . print_r(get_current_user_id(), true));
$favourited_ids = $wpdb->get_col($wpdb->prepare(
"SELECT target_id FROM {$favourites_table}
WHERE user_id = %d AND type IN ($placeholders)",
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 5695d64..f1efa39 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -376,7 +376,7 @@
}
$type = $field_config['type'] ?? 'text';
- $type = $field_config['subType']?:$type;
+ $type = $field_config['subType'] ?? $type;
switch ($type) {
case 'phone':
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index ea6fa2c..a6edcb1 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -198,7 +198,6 @@
*/
protected function getItemLink(int $ID, string $content):string
{
- error_log('Type: '.print_r($content, true));
switch ($content) {
case BASE.'artist':
case BASE.'artwork':
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 742e295..6052ab5 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -1092,10 +1092,6 @@
$args = $this->buildUploadArgs($request);
$data = $request->get_params();
-
-
- error_log('[UploadRoutes]:handleGroupingRequest: data'.print_r($data, true));
- error_log('[UploadRoutes]:handleGroupingRequest: args'.print_r($args, true));
error_log('get_file_params: '.print_r($files, true));
global $_FILES;
error_log('Global Files: '.print_r($_FILES, true));
@@ -1121,7 +1117,7 @@
}
// Queue file upload operation
- $operation_type = $this->determineOperationType($secured_data['files'][0] ?? []);
+ $operation_type = $this->determineOperationType($secured_files['files'][0] ?? []);
$chunkSize = 5;
if ($operation_type === 'video') {
$chunkSize = 1;
@@ -1230,11 +1226,12 @@
$created_posts = [];
$used_upload_ids = [];
+ error_log('Processing Group Data: '.print_r($data, true));
// Create posts from groups
foreach ($data['posts'] as $index => $post) {
$post_title = !empty($post['fields']['post_title'])
? sanitize_text_field($post['fields']['post_title'])
- : 'New ' . JVB_CONTENT[$content]['singular'] . ' ' . ($index + 1);
+ : 'New ' . JVB_CONTENT[$data['content']]['singular'] . ' ' . ($index + 1);
$post_excerpt = !empty($post['fields']['post_excerpt'])
? sanitize_textarea_field($post['fields']['post_excerpt'])
@@ -1282,7 +1279,7 @@
set_post_thumbnail($new_post_id, (int)$gallery_attachment_ids[0]);
array_shift($gallery_attachment_ids);
}
-
+ error_log('Remaining Gallery images: '.print_r($gallery_attachment_ids, true));
// Set gallery images
if (!empty($gallery_attachment_ids)) {
$meta = new MetaManager($new_post_id, 'post');
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index 10b30cb..e8c0478 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -36,7 +36,7 @@
protected string $dataType = '';
protected string $singular = '';
protected string $plural = '';
- protected string $icon = 'triangle';
+ protected string $icon;
// Capabilities
protected array $caps = [];
@@ -134,6 +134,7 @@
protected array $additionalClasses = [];
public function __construct() {
+ $this->icon = jvbDefaultIcon();
$this->user = wp_get_current_user();
$this->user_id = $this->user->ID;
}
diff --git a/inc/ui/Navigation.php b/inc/ui/Navigation.php
index 7988917..16a7e66 100644
--- a/inc/ui/Navigation.php
+++ b/inc/ui/Navigation.php
@@ -30,6 +30,8 @@
private bool $hasToggle = false;
protected array $defaultItemClasses = [];
private int $counter = 0;
+ private bool $isDrawer = false;
+ private bool $drawerCollapsed = true;
public function __construct(string $id = '') {
$this->id = $id ?: 'menu-' . uniqid();
@@ -124,11 +126,19 @@
if ($this->isNav) {
$html = '<nav id="' . esc_attr($this->id).'"' . $classStr.'>';
- if ($this->hasToggle) {
+ // Drawer toggle or regular toggle
+ if ($this->isDrawer) {
+ $html .= '<button class="toggle main" type="button"
+ aria-expanded="' . ($this->drawerCollapsed ? 'false' : 'true') . '"
+ aria-controls="' . esc_attr($this->id) . '-list">
+ ' . jvbIcon('caret-left') . '
+ <span class="screen-reader-text">Toggle Menu</span>
+ </button>';
+ } elseif ($this->hasToggle) {
$html .= '<button class="toggle main" type="button" aria-expanded="false" aria-controls="' . esc_attr($this->id) . '">
- ' . jvbIcon('list') . '
- <span class="screen-reader-text">Toggle Menu</span>
- </button>';
+ ' . jvbIcon('list') . '
+ <span class="screen-reader-text">Toggle Menu</span>
+ </button>';
}
}
if (!$this->isNav) {
@@ -154,6 +164,95 @@
echo $html;
return $html;
}
+
+ /**
+ * Configure as a drawer-style menu
+ *
+ * @param bool $collapsed Initial state
+ * @return self
+ */
+ public function asDrawer(bool $collapsed = true): self {
+ $this->isDrawer = true;
+ $this->drawerCollapsed = $collapsed;
+ $this->addClass('drawer');
+ if (!$collapsed) {
+ $this->addClass('open');
+ }
+ return $this;
+ }
+
+ public function isDrawer(): bool {
+ return $this->isDrawer;
+ }
+ /**
+ * Add a section header
+ *
+ * @param string $title
+ * @return MenuSection
+ */
+ public function addSection(string $title): MenuSection {
+ $section = new MenuSection($title, ++$this->counter);
+ $this->items[] = $section;
+
+ if (!empty($this->defaultItemClasses)) {
+ foreach ($this->defaultItemClasses as $class) {
+ $section->addItemClass($class);
+ }
+ }
+
+ return $section;
+ }
+
+ /**
+ * Populate menu from array structure
+ *
+ * @param array $items Array of menu items
+ * @return self
+ */
+ public function populateFromArray(array $items): self {
+ foreach ($items as $item) {
+ // Handle sections
+ if (!empty($item['section'])) {
+ $section = $this->addSection($item['section']);
+ if (!empty($item['items'])) {
+ $this->populateSection($section, $item['items']);
+ }
+ continue;
+ }
+
+ // Handle regular items
+ $menuItem = $this->addItem($item['text'] ?? '', $item['icon'] ?? '');
+
+ if (!empty($item['class'])) {
+ $menuItem->addClass($item['class']);
+ }
+ if (!empty($item['url'])) {
+ $menuItem->url($item['url']);
+ }
+
+ if (!empty($item['submenu'])) {
+ $submenu = $menuItem->submenu();
+ $submenu->populateFromArray($item['submenu']);
+ }
+ }
+
+ return $this;
+ }
+
+ private function populateSection(MenuSection $section, array $items): void {
+ foreach ($items as $item) {
+ $menuItem = $section->addItem($item['text'] ?? '', $item['icon'] ?? null);
+
+ if (!empty($item['url'])) {
+ $menuItem->url($item['url']);
+ }
+
+ if (!empty($item['submenu'])) {
+ $submenu = $menuItem->submenu();
+ $submenu->populateFromArray($item['submenu']);
+ }
+ }
+ }
}
/**
@@ -324,3 +423,66 @@
return $html;
}
}
+
+/**
+ * Menu section with header and items
+ */
+class MenuSection {
+ private int $id;
+ private string $title;
+ private array $items = [];
+ private array $classes = [];
+ private array $defaultItemClasses = [];
+ private int $counter = 0;
+
+ public function __construct(string $title, int $id) {
+ $this->title = $title;
+ $this->id = $id;
+ }
+
+ public function addItem(?string $text = null, ?string $icon = null): MenuItem {
+ $item = new MenuItem(++$this->counter);
+ $this->items[] = $item;
+
+ if ($text) $item->text($text);
+ if ($icon) $item->icon($icon);
+
+ if (!empty($this->defaultItemClasses)) {
+ foreach ($this->defaultItemClasses as $class) {
+ $item->addClass($class);
+ }
+ }
+
+ return $item;
+ }
+
+ public function addClass(string $class): self {
+ $this->classes[] = $class;
+ return $this;
+ }
+
+ public function addItemClass(string $class): self {
+ $this->defaultItemClasses[] = $class;
+ return $this;
+ }
+
+ public function render(): string {
+ if (empty($this->items)) {
+ return '';
+ }
+
+ $classStr = !empty($this->classes) ? ' class="menu-section ' . esc_attr(implode(' ', $this->classes)) . '"' : ' class="menu-section"';
+
+ $html = '<li' . $classStr . '>';
+ $html .= '<span class="section-title">' . esc_html($this->title) . '</span>';
+ $html .= '<ul class="section-items">';
+
+ foreach ($this->items as $item) {
+ $html .= $item->render();
+ }
+
+ $html .= '</ul></li>';
+
+ return $html;
+ }
+}
diff --git a/inc/utility/Validator.php b/inc/utility/Validator.php
index d6505b2..f77dd52 100644
--- a/inc/utility/Validator.php
+++ b/inc/utility/Validator.php
@@ -342,7 +342,7 @@
$validTypes = [
'text', 'textarea', 'number', 'email', 'url', 'select',
'radio', 'checkbox', 'true_false', 'date', 'time',
- 'datetime', 'color', 'image', 'file', 'gallery',
+ 'datetime', 'color', 'upload', 'image', 'file', 'gallery',
'repeater', 'location', 'user', 'taxonomy', 'set'
];
diff --git a/package-lock.json b/package-lock.json
index 180947c..dfb64fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.0",
"license": "GPL-2.0-or-later",
"devDependencies": {
- "@wordpress/scripts": "^30.26.0"
+ "@wordpress/scripts": "^31.2.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -26,16 +26,37 @@
"node": ">=6.0.0"
}
},
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
@@ -336,9 +357,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
- "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -371,27 +392,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
- "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
- "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.7"
+ "@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1873,28 +1894,25 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.25.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
- "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1920,14 +1938,14 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
- "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.27.1"
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1940,50 +1958,40 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@cacheable/memoize": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@cacheable/memoize/-/memoize-2.0.3.tgz",
- "integrity": "sha512-hl9wfQgpiydhQEIv7fkjEzTGE+tcosCXLKFDO707wYJ/78FVOlowb36djex5GdbSyeHnG62pomYLMuV/OT8Pbw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@cacheable/utils": "^2.0.3"
- }
- },
"node_modules/@cacheable/memory": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.3.tgz",
- "integrity": "sha512-R3UKy/CKOyb1LZG/VRCTMcpiMDyLH7SH3JrraRdK6kf3GweWCOU3sgvE13W3TiDRbxnDKylzKJvhUAvWl9LQOA==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.6.tgz",
+ "integrity": "sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@cacheable/memoize": "^2.0.3",
- "@cacheable/utils": "^2.0.3",
- "@keyv/bigmap": "^1.0.2",
- "hookified": "^1.12.1",
- "keyv": "^5.5.3"
+ "@cacheable/utils": "^2.3.2",
+ "@keyv/bigmap": "^1.3.0",
+ "hookified": "^1.13.0",
+ "keyv": "^5.5.4"
}
},
"node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.1.0.tgz",
- "integrity": "sha512-MX7XIUNwVRK+hjZcAbNJ0Z8DREo+Weu9vinBOjGU1thEi9F6vPhICzBbk4CCf3eEefKRz7n6TfZXwUFZTSgj8Q==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.0.tgz",
+ "integrity": "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "hookified": "^1.12.2"
+ "hashery": "^1.2.0",
+ "hookified": "^1.13.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
- "keyv": "^5.5.3"
+ "keyv": "^5.5.4"
}
},
"node_modules/@cacheable/memory/node_modules/keyv": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
- "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -1992,25 +2000,98 @@
}
},
"node_modules/@cacheable/utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz",
- "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.2.tgz",
+ "integrity": "sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "keyv": "^5.5.3"
+ "hashery": "^1.2.0",
+ "keyv": "^5.5.4"
}
},
"node_modules/@cacheable/utils/node_modules/keyv": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
- "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
@@ -2035,6 +2116,26 @@
"@csstools/css-tokenizer": "^3.0.4"
}
},
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.22.tgz",
+ "integrity": "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
@@ -2101,6 +2202,40 @@
"url": "https://github.com/sponsors/JounQin"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
+ "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+ "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@es-joy/jsdoccomment": {
"version": "0.41.0",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz",
@@ -2149,9 +2284,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2217,9 +2352,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2519,6 +2654,228 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jest/environment-jsdom-abstract": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz",
+ "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.2.0",
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/jsdom": "^21.1.7",
+ "@types/node": "*",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+ "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-mock": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+ "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.2.0",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": {
+ "version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+ "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.5",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": {
+ "version": "0.34.41",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+ "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+ "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+ "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.2.0",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.2.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+ "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+ "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+ "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/@jest/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
@@ -2580,6 +2937,30 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jest/pattern": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
+ "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-regex-util": "30.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/pattern/node_modules/jest-regex-util": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/@jest/reporters": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -2837,6 +3218,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+ "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.10.0"
+ }
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3464,9 +3858,9 @@
}
},
"node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.37.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
- "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
+ "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
@@ -3825,14 +4219,14 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.56.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
- "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
- "playwright": "1.56.1"
+ "playwright": "1.57.0"
},
"bin": {
"playwright": "cli.js"
@@ -3954,9 +4348,9 @@
"license": "MIT"
},
"node_modules/@sentry/core": {
- "version": "9.46.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.46.0.tgz",
- "integrity": "sha512-it7JMFqxVproAgEtbLgCVBYtQ9fIb+Bu0JD+cEplTN/Ukpe6GaolyYib5geZqslVxhp2sQgT+58aGvfd/k0N8Q==",
+ "version": "9.47.1",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz",
+ "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3964,9 +4358,9 @@
}
},
"node_modules/@sentry/node": {
- "version": "9.46.0",
- "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.46.0.tgz",
- "integrity": "sha512-pRLqAcd7GTGvN8gex5FtkQR5Mcol8gOy1WlyZZFq4rBbVtMbqKOQRhohwqnb+YrnmtFpj7IZ7KNDo077MvNeOQ==",
+ "version": "9.47.1",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.47.1.tgz",
+ "integrity": "sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4000,9 +4394,9 @@
"@opentelemetry/sdk-trace-base": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@prisma/instrumentation": "6.11.1",
- "@sentry/core": "9.46.0",
- "@sentry/node-core": "9.46.0",
- "@sentry/opentelemetry": "9.46.0",
+ "@sentry/core": "9.47.1",
+ "@sentry/node-core": "9.47.1",
+ "@sentry/opentelemetry": "9.47.1",
"import-in-the-middle": "^1.14.2",
"minimatch": "^9.0.0"
},
@@ -4011,14 +4405,14 @@
}
},
"node_modules/@sentry/node-core": {
- "version": "9.46.0",
- "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.46.0.tgz",
- "integrity": "sha512-XRVu5pqoklZeh4wqhxCLZkz/ipoKhitctgEFXX9Yh1e1BoHM2pIxT52wf+W6hHM676TFmFXW3uKBjsmRM3AjgA==",
+ "version": "9.47.1",
+ "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.47.1.tgz",
+ "integrity": "sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@sentry/core": "9.46.0",
- "@sentry/opentelemetry": "9.46.0",
+ "@sentry/core": "9.47.1",
+ "@sentry/opentelemetry": "9.47.1",
"import-in-the-middle": "^1.14.2"
},
"engines": {
@@ -4035,13 +4429,13 @@
}
},
"node_modules/@sentry/opentelemetry": {
- "version": "9.46.0",
- "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.46.0.tgz",
- "integrity": "sha512-w2zTxqrdmwRok0cXBoh+ksXdGRUHUZhlpfL/H2kfTodOL+Mk8rW72qUmfqQceXoqgbz8UyK8YgJbyt+XS5H4Qg==",
+ "version": "9.47.1",
+ "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.47.1.tgz",
+ "integrity": "sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@sentry/core": "9.46.0"
+ "@sentry/core": "9.47.1"
},
"engines": {
"node": ">=18"
@@ -4400,16 +4794,6 @@
"url": "https://github.com/sponsors/gregberge"
}
},
- "node_modules/@tootallnate/once": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
- "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@@ -4427,6 +4811,17 @@
"node": ">=10.13.0"
}
},
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4638,9 +5033,9 @@
}
},
"node_modules/@types/jsdom": {
- "version": "20.0.1",
- "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
- "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==",
+ "version": "21.1.7",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
+ "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5219,6 +5614,275 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
+ "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
+ "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
+ "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
+ "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
+ "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
+ "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
+ "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
+ "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
+ "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
+ "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
+ "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
+ "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
+ "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
+ "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
+ "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
+ "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
+ "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
+ "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
+ "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -5428,9 +6092,9 @@
}
},
"node_modules/@wordpress/babel-preset-default": {
- "version": "8.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.33.0.tgz",
- "integrity": "sha512-zi+TfLm7w8UmC/IE1b6/z+GIRMvv9s6yQ7+2a3XUEFriAiLwVM2cRXTcauaKkcos3BDi35M0V8x0T7980RwTlQ==",
+ "version": "8.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.37.0.tgz",
+ "integrity": "sha512-tPLa2YFAau+gT2zKovRK5DRiJchsKgNFiuElIWeKYkLytkyws9fQAgjF/AkDFFxRMdDocNfCscpKWhp79+tIFA==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
@@ -5440,8 +6104,8 @@
"@babel/plugin-transform-runtime": "7.25.7",
"@babel/preset-env": "7.25.7",
"@babel/preset-typescript": "7.25.7",
- "@wordpress/browserslist-config": "^6.33.0",
- "@wordpress/warning": "^3.33.0",
+ "@wordpress/browserslist-config": "^6.37.0",
+ "@wordpress/warning": "^3.37.0",
"browserslist": "^4.21.10",
"core-js": "^3.31.0",
"react": "^18.3.0"
@@ -5616,9 +6280,9 @@
}
},
"node_modules/@wordpress/base-styles": {
- "version": "6.9.0",
- "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.9.0.tgz",
- "integrity": "sha512-z3WCO0EdVWrXkEn6QXlFQZoKyPxplIctOWTqG8KPLtdHa0gqXhF+gaNxwGg6Ao2ac4sqoFSBcKPhXgE/08jK7g==",
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.13.0.tgz",
+ "integrity": "sha512-+APLd5GqzzJ/atVVs3LGPcCRRy8mVfVQi1QY+cseNAQbRe4LvsDarLbzkblWEwuksxgUGmVGDC3fDNxrwszJ2A==",
"dev": true,
"license": "GPL-2.0-or-later",
"engines": {
@@ -5627,9 +6291,9 @@
}
},
"node_modules/@wordpress/browserslist-config": {
- "version": "6.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.33.0.tgz",
- "integrity": "sha512-4plw8mLKjcd1beuJzmjT4GNBk+R02qu/og6h/BuGMY8dxfqovfGB0Z2w7C85ILmjY2qnvsU7gelDcSXNgwuwxQ==",
+ "version": "6.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.37.0.tgz",
+ "integrity": "sha512-L//FRak9bo+sDLAignC4QkhITHgeFVlL0C4lWI/AM+AIHKGrHT4LOdwwNpYWMkkztW9rxHRptGkF/JDxuCakxQ==",
"dev": true,
"license": "GPL-2.0-or-later",
"engines": {
@@ -5638,9 +6302,9 @@
}
},
"node_modules/@wordpress/dependency-extraction-webpack-plugin": {
- "version": "6.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.33.0.tgz",
- "integrity": "sha512-uGvJrak1wpi6XAfIvSXedXgfxvavpzVlj7ypAedAqQ26eFLHCPzK9S2TRp+jw4BglUE3mR2NXD8/glorbGwq+g==",
+ "version": "6.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.37.0.tgz",
+ "integrity": "sha512-N9DTLr07AYV9vo/kFVSKYp/+wg2wBHL4ekDldwx2rGNtOHjoSi1oo+iBmStSaKMRtCQX2Xq8xVFH04+53T0yXA==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
@@ -5662,14 +6326,13 @@
"license": "BSD"
},
"node_modules/@wordpress/e2e-test-utils-playwright": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.33.0.tgz",
- "integrity": "sha512-OuxF/5TeHh2k58jsKRG2AtFhoRgAFKUrOjcrBLaNew3Y6RepwvLLgSq1LXqUrR1nhJU90AaH6AqFrJ2s+lmFUw==",
+ "version": "1.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.37.0.tgz",
+ "integrity": "sha512-H77He9+sPNchzbEeFk2YSuYW5JNk3oHnlXJ1EVPUHpIZTr6gM80AGBuxP/W4WtWdLn0A5/TxCmTAxsN1M8YVvQ==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
"change-case": "^4.1.2",
- "form-data": "^4.0.0",
"get-port": "^5.1.1",
"lighthouse": "^12.2.2",
"mime": "^3.0.0",
@@ -5684,19 +6347,20 @@
}
},
"node_modules/@wordpress/eslint-plugin": {
- "version": "22.19.0",
- "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-22.19.0.tgz",
- "integrity": "sha512-J24RZ6U4Ref0ix8uhmc3XJGkJLdi/V+JOQjjRwB0uLpsSHio4+LhAJrBlovkZCf+0HsRKiJHuIdli0EKW5gl3g==",
+ "version": "23.0.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-23.0.0.tgz",
+ "integrity": "sha512-fdgBWc7jC0JKi8j16lt51F3Sp02n7emSU3af5UFnQ5feXyJSO3mGcVJzZDpehL3ts/Clhu3II+CYOlUeek4H8g==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
"@babel/eslint-parser": "7.25.7",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
- "@wordpress/babel-preset-default": "^8.33.0",
- "@wordpress/prettier-config": "^4.33.0",
+ "@wordpress/babel-preset-default": "^8.37.0",
+ "@wordpress/prettier-config": "^4.37.0",
"cosmiconfig": "^7.0.0",
"eslint-config-prettier": "^8.3.0",
+ "eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jest": "^27.4.3",
"eslint-plugin-jsdoc": "^46.4.6",
@@ -5774,9 +6438,9 @@
}
},
"node_modules/@wordpress/jest-console": {
- "version": "8.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.33.0.tgz",
- "integrity": "sha512-G9mJYPpGokk+G5MCM2xMQzHqmZY2DNTFDxtJnmH4ISHm4+2S2OTsHovTNuOM+n8QqaaB2En4uuBfYykpRQfNlw==",
+ "version": "8.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.37.0.tgz",
+ "integrity": "sha512-RTNy0qg1sQLRunqsWjCAR8Nw2RZ62DSIaf09J6xzmuYdFjdvsmeQWs0OSs9e6mVQftxqDgJ/V4NQ1ctLxZM06w==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
@@ -5791,13 +6455,13 @@
}
},
"node_modules/@wordpress/jest-preset-default": {
- "version": "12.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.33.0.tgz",
- "integrity": "sha512-TI3FHvMyWeC36IBz7lGaADLIHrSow9Yj80jwisWZ1uppWkAh1wwnJuGnMUn6dSydUolCGitLcMBjA/kGx3uPLw==",
+ "version": "12.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.37.0.tgz",
+ "integrity": "sha512-AaT02lS2a/FnD/arYSSc1TkbIlLd1zXY40YqGtpYz1/MqJKoje6th/RZGOMQKC1pyXt5KKBzSFMD4Av39CxThw==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
- "@wordpress/jest-console": "^8.33.0",
+ "@wordpress/jest-console": "^8.37.0",
"babel-jest": "29.7.0"
},
"engines": {
@@ -5810,9 +6474,9 @@
}
},
"node_modules/@wordpress/npm-package-json-lint-config": {
- "version": "5.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.33.0.tgz",
- "integrity": "sha512-XejRL8yPGoBVY44gvfH2A2STzFDUjzT7inxhsqzZWYgpMtDNjgdrRN6fgA1GP1nyQx0iRg28r/vapjFCWCA+5w==",
+ "version": "5.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.37.0.tgz",
+ "integrity": "sha512-Ond/mR+fw+8JMlcMmj1MBjVgQKD7GVYlL3Ww1n1/yBpRS9OP+UECF4vES3rdEqvL8TU+O/cvoWFpNY2DWO4P8w==",
"dev": true,
"license": "GPL-2.0-or-later",
"engines": {
@@ -5824,13 +6488,13 @@
}
},
"node_modules/@wordpress/postcss-plugins-preset": {
- "version": "5.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.33.0.tgz",
- "integrity": "sha512-VBmXyBpjq96L58ox5Fmhc2lMKuLZafqkz8im34gQOthjw8PwkHXDCcC/q5ue5SzYXvX07UTZnGGuc7V6ARrHLg==",
+ "version": "5.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.37.0.tgz",
+ "integrity": "sha512-6+/aFPjr3AhmiBQEZs/huG3pkHPUHwwqwS8IIUqwDFuXnX3RkqRJvIoLdfDyR04QaFamyk1IoRv01zU0pZB8YA==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
- "@wordpress/base-styles": "^6.9.0",
+ "@wordpress/base-styles": "^6.13.0",
"autoprefixer": "^10.4.20",
"postcss-import": "^16.1.1"
},
@@ -5843,9 +6507,9 @@
}
},
"node_modules/@wordpress/prettier-config": {
- "version": "4.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.33.0.tgz",
- "integrity": "sha512-PRNb10ouWjg52yeWHTXlaZqkuHMSHlKq9Risg368f5fWU7akDJgZboiD6jVdtv+iGXdFRlI5oRF31wqArzNykA==",
+ "version": "4.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.37.0.tgz",
+ "integrity": "sha512-5l+5q07DXpoMhGWxVjJqb60RFyspaD7twCnnVWzqfFNUfKWptytMj3KYV3oO9+4ONWehsEzRF9zl3ai+S6ztvg==",
"dev": true,
"license": "GPL-2.0-or-later",
"engines": {
@@ -5857,25 +6521,25 @@
}
},
"node_modules/@wordpress/scripts": {
- "version": "30.26.0",
- "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.26.0.tgz",
- "integrity": "sha512-RpyF41xHtA4ktOP0JBBb6/MkoB7/H/emqQnO3t+dZFs56jCP/8141MicDl7Ne9PY29D4NaB0LgbcmthK5Msk1Q==",
+ "version": "31.2.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-31.2.0.tgz",
+ "integrity": "sha512-0BKF/aWGX2MegnsgxNkoKQCIlZzvzc/w9noC0hCsNCQqgTolP+idtr8sBA3J2E5gP3ieGk4DCr9CbAtLjkP23g==",
"dev": true,
"license": "GPL-2.0-or-later",
"dependencies": {
"@babel/core": "7.25.7",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@svgr/webpack": "^8.0.1",
- "@wordpress/babel-preset-default": "^8.33.0",
- "@wordpress/browserslist-config": "^6.33.0",
- "@wordpress/dependency-extraction-webpack-plugin": "^6.33.0",
- "@wordpress/e2e-test-utils-playwright": "^1.33.0",
- "@wordpress/eslint-plugin": "^22.19.0",
- "@wordpress/jest-preset-default": "^12.33.0",
- "@wordpress/npm-package-json-lint-config": "^5.33.0",
- "@wordpress/postcss-plugins-preset": "^5.33.0",
- "@wordpress/prettier-config": "^4.33.0",
- "@wordpress/stylelint-config": "^23.25.0",
+ "@wordpress/babel-preset-default": "^8.37.0",
+ "@wordpress/browserslist-config": "^6.37.0",
+ "@wordpress/dependency-extraction-webpack-plugin": "^6.37.0",
+ "@wordpress/e2e-test-utils-playwright": "^1.37.0",
+ "@wordpress/eslint-plugin": "^23.0.0",
+ "@wordpress/jest-preset-default": "^12.37.0",
+ "@wordpress/npm-package-json-lint-config": "^5.37.0",
+ "@wordpress/postcss-plugins-preset": "^5.37.0",
+ "@wordpress/prettier-config": "^4.37.0",
+ "@wordpress/stylelint-config": "^23.29.0",
"adm-zip": "^0.5.9",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
@@ -5894,7 +6558,7 @@
"filenamify": "^4.2.0",
"jest": "^29.6.2",
"jest-dev-server": "^10.1.4",
- "jest-environment-jsdom": "^29.6.2",
+ "jest-environment-jsdom": "^30.2.0",
"jest-environment-node": "^29.6.2",
"json2php": "^0.0.9",
"markdownlint-cli": "^0.31.1",
@@ -5931,7 +6595,7 @@
"npm": ">=8.19.2"
},
"peerDependencies": {
- "@playwright/test": "^1.55.0",
+ "@playwright/test": "^1.57.0",
"@wordpress/env": "^10.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
@@ -5943,9 +6607,9 @@
}
},
"node_modules/@wordpress/stylelint-config": {
- "version": "23.25.0",
- "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.25.0.tgz",
- "integrity": "sha512-GefqayI9kSohIwYW6xkK8jZTF62k71ALdMSVgktMXru567gUDpb1Ci79CIY4iTK3fq/OpJW3uAM4AfXYNH8+3Q==",
+ "version": "23.29.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.29.0.tgz",
+ "integrity": "sha512-TTT2oc9Fw2rdnCs4bzI2EHRXgODT3YggjcewqxbBcioPsiAOff9E9gp6NZhe2VlZjVsDcwabUs6m4RIgbBSKaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5963,9 +6627,9 @@
}
},
"node_modules/@wordpress/warning": {
- "version": "3.33.0",
- "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.33.0.tgz",
- "integrity": "sha512-LzYgKfxgK5YEpTu4zHPCDzw+kH5hYCrKRK/joK8S9booy5ERvzRCPrISMwrmAKTD9esYF82+IEHhW0/qsjxPsw==",
+ "version": "3.37.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.37.0.tgz",
+ "integrity": "sha512-oXWyKiYJIa9SuPRNEJiOWn2Qk0RzfxOsDqXcus1OL44swCRtSM+ypm16CJpRhZpMUcsJ6d23PBxTC97C/iiJpQ==",
"dev": true,
"license": "GPL-2.0-or-later",
"engines": {
@@ -6033,17 +6697,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/acorn-globals": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz",
- "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.1.0",
- "acorn-walk": "^8.0.2"
- }
- },
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
@@ -6088,16 +6741,13 @@
}
},
"node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "debug": "4"
- },
"engines": {
- "node": ">= 6.0.0"
+ "node": ">= 14"
}
},
"node_modules/ajv": {
@@ -6547,19 +7197,20 @@
"license": "MIT"
},
"node_modules/atomically": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
- "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz",
+ "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "stubborn-fs": "^1.2.5",
- "when-exit": "^2.1.1"
+ "stubborn-fs": "^2.0.0",
+ "when-exit": "^2.1.4"
}
},
"node_modules/autoprefixer": {
- "version": "10.4.21",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
- "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
"funding": [
{
@@ -6577,10 +7228,9 @@
],
"license": "MIT",
"dependencies": {
- "browserslist": "^4.24.4",
- "caniuse-lite": "^1.0.30001702",
- "fraction.js": "^4.3.7",
- "normalize-range": "^0.1.2",
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
@@ -6621,14 +7271,14 @@
}
},
"node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -6926,6 +7576,16 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.11",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+ "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/basic-ftp": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
@@ -7063,9 +7723,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -7084,10 +7744,11 @@
"license": "MIT",
"peer": true,
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -7172,24 +7833,23 @@
}
},
"node_modules/cacheable": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.1.1.tgz",
- "integrity": "sha512-LmF4AXiSNdiRbI2UjH8pAp9NIXxeQsTotpEaegPiDcnN0YPygDJDV3l/Urc0mL72JWdATEorKqIHEx55nDlONg==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.1.tgz",
+ "integrity": "sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@cacheable/memoize": "^2.0.3",
- "@cacheable/memory": "^2.0.3",
- "@cacheable/utils": "^2.1.0",
- "hookified": "^1.12.2",
- "keyv": "^5.5.3",
- "qified": "^0.5.0"
+ "@cacheable/memory": "^2.0.6",
+ "@cacheable/utils": "^2.3.2",
+ "hookified": "^1.14.0",
+ "keyv": "^5.5.5",
+ "qified": "^0.5.3"
}
},
"node_modules/cacheable/node_modules/keyv": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz",
- "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==",
+ "version": "5.5.5",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz",
+ "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7322,9 +7982,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001731",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
- "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
+ "version": "1.0.30001761",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
+ "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"dev": true,
"funding": [
{
@@ -7670,9 +8330,9 @@
}
},
"node_modules/compression": {
- "version": "1.7.5",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
- "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7680,7 +8340,7 @@
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
- "on-headers": "~1.0.2",
+ "on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
@@ -7873,9 +8533,9 @@
}
},
"node_modules/core-js": {
- "version": "3.46.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
- "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -7952,9 +8612,9 @@
"license": "Python-2.0"
},
"node_modules/cosmiconfig/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8252,33 +8912,20 @@
"dev": true,
"license": "CC0-1.0"
},
- "node_modules/cssom": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
- "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cssstyle": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
- "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cssom": "~0.3.6"
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
}
},
- "node_modules/cssstyle/node_modules/cssom": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
- "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/cwd": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
@@ -8311,18 +8958,17 @@
}
},
"node_modules/data-urls": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
- "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "abab": "^2.0.6",
- "whatwg-mimetype": "^3.0.0",
- "whatwg-url": "^11.0.0"
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/data-view-buffer": {
@@ -8449,9 +9095,9 @@
"license": "MIT"
},
"node_modules/dedent": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
- "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
+ "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -8711,20 +9357,6 @@
],
"license": "BSD-2-Clause"
},
- "node_modules/domexception": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
- "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
- "deprecated": "Use your platform's native DOMException instead",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
@@ -8826,9 +9458,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.91",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.91.tgz",
- "integrity": "sha512-sNSHHyq048PFmZY4S90ax61q+gLCs0X0YmcOII9wG9S2XwbVr+h4VW2wWhnbp/Eys3cCwTxVF292W3qPaxIapQ==",
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"dev": true,
"license": "ISC"
},
@@ -8967,9 +9599,9 @@
}
},
"node_modules/es-abstract": {
- "version": "1.24.0",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
- "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+ "version": "1.24.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+ "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9056,27 +9688,27 @@
}
},
"node_modules/es-iterator-helpers": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
- "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
+ "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
- "call-bound": "^1.0.3",
+ "call-bound": "^1.0.4",
"define-properties": "^1.2.1",
- "es-abstract": "^1.23.6",
+ "es-abstract": "^1.24.1",
"es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.0.3",
+ "es-set-tostringtag": "^2.1.0",
"function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.6",
+ "get-intrinsic": "^1.3.0",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.2.0",
"has-symbols": "^1.1.0",
"internal-slot": "^1.1.0",
- "iterator.prototype": "^1.1.4",
+ "iterator.prototype": "^1.1.5",
"safe-array-concat": "^1.1.3"
},
"engines": {
@@ -9285,6 +9917,31 @@
"eslint": ">=7.0.0"
}
},
+ "node_modules/eslint-import-context": {
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz",
+ "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-tsconfig": "^4.10.1",
+ "stable-hash-x": "^0.2.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-context"
+ },
+ "peerDependencies": {
+ "unrs-resolver": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "unrs-resolver": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-import-resolver-node": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -9307,6 +9964,41 @@
"ms": "^2.1.1"
}
},
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz",
+ "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "debug": "^4.4.1",
+ "eslint-import-context": "^0.1.8",
+ "get-tsconfig": "^4.10.1",
+ "is-bun-module": "^2.0.0",
+ "stable-hash-x": "^0.2.0",
+ "tinyglobby": "^0.2.14",
+ "unrs-resolver": "^1.7.11"
+ },
+ "engines": {
+ "node": "^16.17.0 || >=18.6.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-resolver-typescript"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-module-utils": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
@@ -9341,6 +10033,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -9928,9 +10621,9 @@
}
},
"node_modules/eslint/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10695,14 +11388,16 @@
}
},
"node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -10727,16 +11422,16 @@
"license": "MIT"
},
"node_modules/fraction.js": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
- "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
- "type": "patreon",
+ "type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
@@ -10966,6 +11661,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
+ "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
@@ -11024,9 +11732,9 @@
"license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11286,6 +11994,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hashery": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.3.0.tgz",
+ "integrity": "sha512-fWltioiy5zsSAs9ouEnvhsVJeAXRybGCNNv0lvzpzNOSDbULXRy7ivFWwCCv4I5Am6kSo75hmbsCduOoc2/K4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.13.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -11324,9 +12045,9 @@
}
},
"node_modules/hookified": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.2.tgz",
- "integrity": "sha512-aokUX1VdTpI0DUsndvW+OiwmBpKCu/NgRsSSkuSY0zq8PY6Q6a+lmOfAFDXAAOtBqJELvcWY9L1EVtzjbQcMdg==",
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.14.0.tgz",
+ "integrity": "sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==",
"dev": true,
"license": "MIT"
},
@@ -11417,16 +12138,16 @@
}
},
"node_modules/html-encoding-sniffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
- "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "whatwg-encoding": "^2.0.0"
+ "whatwg-encoding": "^3.1.1"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/html-entities": {
@@ -11523,24 +12244,23 @@
}
},
"node_modules/http-proxy-agent": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
- "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tootallnate/once": "2",
- "agent-base": "6",
- "debug": "4"
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
},
"engines": {
- "node": ">= 6"
+ "node": ">= 14"
}
},
"node_modules/http-proxy-middleware": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
- "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+ "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11563,17 +12283,17 @@
}
},
"node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "agent-base": "6",
+ "agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
- "node": ">= 6"
+ "node": ">= 14"
}
},
"node_modules/human-signals": {
@@ -11657,9 +12377,9 @@
}
},
"node_modules/ignore-walk/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11995,6 +12715,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-bun-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+ "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.7.1"
+ }
+ },
+ "node_modules/is-bun-module/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -12795,26 +13538,23 @@
}
},
"node_modules/jest-environment-jsdom": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz",
- "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==",
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz",
+ "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/environment": "^29.7.0",
- "@jest/fake-timers": "^29.7.0",
- "@jest/types": "^29.6.3",
- "@types/jsdom": "^20.0.0",
+ "@jest/environment": "30.2.0",
+ "@jest/environment-jsdom-abstract": "30.2.0",
+ "@types/jsdom": "^21.1.7",
"@types/node": "*",
- "jest-mock": "^29.7.0",
- "jest-util": "^29.7.0",
- "jsdom": "^20.0.0"
+ "jsdom": "^26.1.0"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
- "canvas": "^2.5.0"
+ "canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
@@ -12822,6 +13562,200 @@
}
}
},
+ "node_modules/jest-environment-jsdom/node_modules/@jest/environment": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+ "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-mock": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+ "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.2.0",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": {
+ "version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/types": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+ "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.5",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": {
+ "version": "0.34.41",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+ "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/ci-info": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+ "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-message-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+ "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.2.0",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.2.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-mock": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+ "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/jest-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+ "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/pretty-format": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+ "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/jest-environment-node": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
@@ -13244,9 +14178,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13268,44 +14202,39 @@
}
},
"node_modules/jsdom": {
- "version": "20.0.3",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz",
- "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "abab": "^2.0.6",
- "acorn": "^8.8.1",
- "acorn-globals": "^7.0.0",
- "cssom": "^0.5.0",
- "cssstyle": "^2.3.0",
- "data-urls": "^3.0.2",
- "decimal.js": "^10.4.2",
- "domexception": "^4.0.0",
- "escodegen": "^2.0.0",
- "form-data": "^4.0.0",
- "html-encoding-sniffer": "^3.0.0",
- "http-proxy-agent": "^5.0.0",
- "https-proxy-agent": "^5.0.1",
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.2",
- "parse5": "^7.1.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
- "tough-cookie": "^4.1.2",
- "w3c-xmlserializer": "^4.0.0",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
- "whatwg-encoding": "^2.0.0",
- "whatwg-mimetype": "^3.0.0",
- "whatwg-url": "^11.0.0",
- "ws": "^8.11.0",
- "xml-name-validator": "^4.0.0"
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
},
"peerDependencies": {
- "canvas": "^2.5.0"
+ "canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
@@ -13582,9 +14511,9 @@
"license": "Apache-2.0"
},
"node_modules/lighthouse/node_modules/@puppeteer/browsers": {
- "version": "2.10.12",
- "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz",
- "integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz",
+ "integrity": "sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -13604,18 +14533,18 @@
}
},
"node_modules/lighthouse/node_modules/puppeteer-core": {
- "version": "24.25.0",
- "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.25.0.tgz",
- "integrity": "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ==",
+ "version": "24.34.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.34.0.tgz",
+ "integrity": "sha512-24evawO+mUGW4mvS2a2ivwLdX3gk8zRLZr9HP+7+VT2vBQnm0oh9jJEZmUE3ePJhRkYlZ93i7OMpdcoi2qNCLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@puppeteer/browsers": "2.10.12",
- "chromium-bidi": "9.1.0",
+ "@puppeteer/browsers": "2.11.0",
+ "chromium-bidi": "12.0.1",
"debug": "^4.4.3",
- "devtools-protocol": "0.0.1508733",
+ "devtools-protocol": "0.0.1534754",
"typed-query-selector": "^2.12.0",
- "webdriver-bidi-protocol": "0.3.7",
+ "webdriver-bidi-protocol": "0.3.10",
"ws": "^8.18.3"
},
"engines": {
@@ -13623,9 +14552,9 @@
}
},
"node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
- "integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-12.0.1.tgz",
+ "integrity": "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -13637,9 +14566,9 @@
}
},
"node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": {
- "version": "0.0.1508733",
- "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
- "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
+ "version": "0.0.1534754",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
+ "integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
@@ -13787,9 +14716,9 @@
"license": "MIT"
},
"node_modules/lodash-es": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
- "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "version": "4.17.22",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+ "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
"dev": true,
"license": "MIT"
},
@@ -14038,9 +14967,9 @@
"license": "Python-2.0"
},
"node_modules/markdownlint-cli/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14069,9 +14998,9 @@
}
},
"node_modules/markdownlint-cli/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14522,6 +15451,22 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-postinstall": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -14576,9 +15521,9 @@
"optional": true
},
"node_modules/node-forge": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
- "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
+ "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
"dev": true,
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
@@ -14593,9 +15538,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
@@ -14638,16 +15583,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/npm-bundled": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz",
@@ -14778,9 +15713,9 @@
}
},
"node_modules/nwsapi": {
- "version": "2.2.16",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz",
- "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"dev": true,
"license": "MIT"
},
@@ -14935,9 +15870,9 @@
}
},
"node_modules/on-headers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -15133,44 +16068,6 @@
"node": ">= 14"
}
},
- "node_modules/pac-proxy-agent/node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
@@ -15245,18 +16142,31 @@
}
},
"node_modules/parse5": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
- "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "entities": "^4.5.0"
+ "entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -15529,13 +16439,13 @@
}
},
"node_modules/playwright": {
- "version": "1.56.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
- "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.56.1"
+ "playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
@@ -15548,9 +16458,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.56.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
- "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -16327,9 +17237,9 @@
}
},
"node_modules/postgres-bytea": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
- "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -16522,44 +17432,6 @@
"node": ">= 14"
}
},
- "node_modules/proxy-agent/node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/proxy-agent/node_modules/http-proxy-agent": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
- "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/proxy-agent/node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.1.2",
- "debug": "4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -16577,19 +17449,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/psl": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
- "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/lupomontero"
- }
- },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -16654,13 +17513,13 @@
"license": "MIT"
},
"node_modules/qified": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.1.tgz",
- "integrity": "sha512-+BtFN3dCP+IaFA6IYNOu/f/uK1B8xD2QWyOeCse0rjtAebBmkzgd2d1OAXi3ikAzJMIBSdzZDNZ3wZKEUDQs5w==",
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.5.3.tgz",
+ "integrity": "sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "hookified": "^1.12.2"
+ "hookified": "^1.13.0"
},
"engines": {
"node": ">=20"
@@ -16682,13 +17541,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/querystringify": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
- "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -17009,13 +17861,6 @@
"node": ">=4"
}
},
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/regenerator-transform": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
@@ -17218,6 +18063,16 @@
"node": ">=8"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/resolve.exports": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@@ -17276,6 +18131,13 @@
"node": ">=10.0.0"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rtlcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
@@ -18137,16 +18999,6 @@
"node": ">= 14"
}
},
- "node_modules/socks-proxy-agent/node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -18338,6 +19190,16 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/stable-hash-x": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
+ "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -18646,10 +19508,21 @@
}
},
"node_modules/stubborn-fs": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
- "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==",
- "dev": true
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
+ "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stubborn-utils": "^1.0.1"
+ }
+ },
+ "node_modules/stubborn-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
+ "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/style-search": {
"version": "0.1.0",
@@ -18676,9 +19549,9 @@
}
},
"node_modules/stylelint": {
- "version": "16.25.0",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz",
- "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==",
+ "version": "16.26.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz",
+ "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==",
"dev": true,
"funding": [
{
@@ -18694,6 +19567,7 @@
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.19",
"@csstools/css-tokenizer": "^3.0.4",
"@csstools/media-query-list-parser": "^4.0.3",
"@csstools/selector-specificity": "^5.0.0",
@@ -18706,7 +19580,7 @@
"debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
- "file-entry-cache": "^10.1.4",
+ "file-entry-cache": "^11.1.1",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
@@ -18787,46 +19661,39 @@
}
},
"node_modules/stylelint-scss": {
- "version": "6.12.1",
- "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.12.1.tgz",
- "integrity": "sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==",
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.13.0.tgz",
+ "integrity": "sha512-kZPwFUJkfup2gP1enlrS2h9U5+T5wFoqzJ1n/56AlpwSj28kmFe7ww/QFydvPsg5gLjWchAwWWBLtterynZrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"css-tree": "^3.0.1",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.36.0",
- "mdn-data": "^2.21.0",
+ "known-css-properties": "^0.37.0",
+ "mdn-data": "^2.25.0",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.6",
- "postcss-selector-parser": "^7.1.0",
+ "postcss-selector-parser": "^7.1.1",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": ">=18.12.0"
},
"peerDependencies": {
- "stylelint": "^16.0.2"
+ "stylelint": "^16.8.2"
}
},
- "node_modules/stylelint-scss/node_modules/known-css-properties": {
- "version": "0.36.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz",
- "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/stylelint-scss/node_modules/mdn-data": {
- "version": "2.24.0",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.24.0.tgz",
- "integrity": "sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==",
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.25.0.tgz",
+ "integrity": "sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/stylelint-scss/node_modules/postcss-selector-parser": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
- "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+ "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": {
@@ -18926,25 +19793,25 @@
}
},
"node_modules/stylelint/node_modules/file-entry-cache": {
- "version": "10.1.4",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz",
- "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==",
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.1.tgz",
+ "integrity": "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "flat-cache": "^6.1.13"
+ "flat-cache": "^6.1.19"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
- "version": "6.1.18",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.18.tgz",
- "integrity": "sha512-JUPnFgHMuAVmLmoH9/zoZ6RHOt5n9NlUw/sDXsTbROJ2SFoS2DS4s+swAV6UTeTbGH/CAsZIE6M8TaG/3jVxgQ==",
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.19.tgz",
+ "integrity": "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cacheable": "^2.1.0",
+ "cacheable": "^2.2.0",
"flatted": "^3.3.3",
- "hookified": "^1.12.0"
+ "hookified": "^1.13.0"
}
},
"node_modules/stylelint/node_modules/global-modules": {
@@ -18986,9 +19853,9 @@
}
},
"node_modules/stylelint/node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19022,9 +19889,9 @@
}
},
"node_modules/stylelint/node_modules/postcss-selector-parser": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
- "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+ "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",
"peer": true,
@@ -19510,23 +20377,92 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
"node_modules/tldts-core": {
- "version": "7.0.17",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz",
- "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==",
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
+ "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
"dev": true,
"license": "MIT"
},
"node_modules/tldts-icann": {
- "version": "7.0.17",
- "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.17.tgz",
- "integrity": "sha512-up4oFDoumyz2RscRxoYRxf+2OvIKUHjh7rUvuGWI0PZ/47k35sadoi2JyKR0AIfTw09qcfix8bUxXFQhY1QZIQ==",
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.0.19.tgz",
+ "integrity": "sha512-PZgda8E2cXMNa7QlBbiZh3vcS8UaPTDRIBmcGPDlujSMtQLrzjvikeJxzQSqWxn3muaMJ7BsC+aL464Yl2I6cA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.0.17"
+ "tldts-core": "^7.0.19"
}
},
+ "node_modules/tldts/node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -19568,32 +20504,29 @@
}
},
"node_modules/tough-cookie": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
- "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
- "psl": "^1.1.33",
- "punycode": "^2.1.1",
- "universalify": "^0.2.0",
- "url-parse": "^1.5.3"
+ "tldts": "^6.1.32"
},
"engines": {
- "node": ">=6"
+ "node": ">=16"
}
},
"node_modules/tr46": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
- "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "punycode": "^2.1.1"
+ "punycode": "^2.3.1"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/tree-kill": {
@@ -19957,16 +20890,6 @@
"node": ">=4"
}
},
- "node_modules/universalify": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
- "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -19977,10 +20900,46 @@
"node": ">= 0.8"
}
},
+ "node_modules/unrs-resolver": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
+ "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "napi-postinstall": "^0.3.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
+ "@unrs/resolver-binding-android-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-arm64": "1.11.1",
+ "@unrs/resolver-binding-darwin-x64": "1.11.1",
+ "@unrs/resolver-binding-freebsd-x64": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
+ "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
+ "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+ }
+ },
"node_modules/update-browserslist-db": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
- "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
@@ -20085,17 +21044,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/url-parse": {
- "version": "1.5.10",
- "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
- "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "querystringify": "^2.1.1",
- "requires-port": "^1.0.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -20181,16 +21129,16 @@
}
},
"node_modules/w3c-xmlserializer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz",
- "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "xml-name-validator": "^4.0.0"
+ "xml-name-validator": "^5.0.0"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
}
},
"node_modules/wait-on": {
@@ -20255,9 +21203,9 @@
"license": "Apache-2.0"
},
"node_modules/webdriver-bidi-protocol": {
- "version": "0.3.7",
- "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.7.tgz",
- "integrity": "sha512-wIx5Gu/LLTeexxilpk8WxU2cpGAKlfbWRO5h+my6EMD1k5PYqM1qQO1MHUFf4f3KRnhBvpbZU7VkizAgeSEf7g==",
+ "version": "0.3.10",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.10.tgz",
+ "integrity": "sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw==",
"dev": true,
"license": "Apache-2.0"
},
@@ -20692,46 +21640,46 @@
}
},
"node_modules/whatwg-encoding": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
- "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
- "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/whatwg-url": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
- "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tr46": "^3.0.0",
+ "tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/when-exit": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
- "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==",
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
+ "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
"dev": true,
"license": "MIT"
},
@@ -20932,13 +21880,13 @@
}
},
"node_modules/xml-name-validator": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
- "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/xmlchars": {
diff --git a/package.json b/package.json
index 0ccfb57..705c424 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,6 @@
"start": "wp-scripts start"
},
"devDependencies": {
- "@wordpress/scripts": "^30.26.0"
+ "@wordpress/scripts": "^31.2.0"
}
}
diff --git a/src/drawer-menu/block.json b/src/drawer-menu/block.json
new file mode 100644
index 0000000..93bc73d
--- /dev/null
+++ b/src/drawer-menu/block.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/drawer-menu",
+ "title": "Drawer Menu",
+ "category": "jvb",
+ "icon": "menu",
+ "version": "1.0.0",
+ "textdomain": "jvb",
+ "supports": {
+ "html": false
+ },
+ "attributes": {
+ "menuId": {
+ "type": "string",
+ "default": ""
+ },
+ "collapsed": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
diff --git a/src/drawer-menu/edit.js b/src/drawer-menu/edit.js
new file mode 100644
index 0000000..c37544b
--- /dev/null
+++ b/src/drawer-menu/edit.js
@@ -0,0 +1,33 @@
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, ToggleControl, TextControl } from '@wordpress/components';
+
+export default function Edit({ attributes, setAttributes }) {
+ const { menuId, collapsed } = attributes;
+ const blockProps = useBlockProps();
+
+ return (
+ <>
+ <InspectorControls>
+ <PanelBody title="Drawer Settings">
+ <TextControl
+ label="Menu ID"
+ value={menuId}
+ onChange={(value) => setAttributes({ menuId: value })}
+ help="PHP-generated menu identifier"
+ />
+ <ToggleControl
+ label="Start Collapsed"
+ checked={collapsed}
+ onChange={(value) => setAttributes({ collapsed: value })}
+ />
+ </PanelBody>
+ </InspectorControls>
+ <div {...blockProps}>
+ <div className="drawer-menu-preview">
+ <p>Drawer Menu: {menuId || 'Not configured'}</p>
+ <p>State: {collapsed ? 'Collapsed' : 'Expanded'}</p>
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/src/drawer-menu/editor.scss b/src/drawer-menu/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/drawer-menu/editor.scss
diff --git a/src/drawer-menu/index.js b/src/drawer-menu/index.js
new file mode 100644
index 0000000..e81333b
--- /dev/null
+++ b/src/drawer-menu/index.js
@@ -0,0 +1,10 @@
+import { registerBlockType } from '@wordpress/blocks';
+import edit from './edit';
+import save from './save';
+import './style.scss';
+import './editor.scss';
+
+registerBlockType('jvb/drawer-menu', {
+ edit,
+ save,
+});
diff --git a/src/drawer-menu/index.php b/src/drawer-menu/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/drawer-menu/index.php
diff --git a/src/drawer-menu/render.php b/src/drawer-menu/render.php
new file mode 100644
index 0000000..d0826a3
--- /dev/null
+++ b/src/drawer-menu/render.php
@@ -0,0 +1,39 @@
+<?php
+
+use JVBase\managers\CacheManager;
+use JVBase\ui\Navigation;
+
+$menu_id = $attributes['menuId'] ?? '';
+$collapsed = $attributes['collapsed'] ?? true;
+
+// You'd populate this from options, a filter, or however you store menu data
+$menu_items = apply_filters('jvbDrawerItems', [], $menu_id);
+
+if (empty($menu_items) || empty($menu_id)) {
+ return '<p>Please configure the drawer menu in block settings.</p>';
+}
+
+$cache = CacheManager::for('drawer');
+$cache->clear();
+
+if (!is_front_page()) {
+ $menu_items[] = [
+ 'text' => 'Home',
+ 'url' => home_url(),
+ 'icon' => 'house-simple',
+ ];
+}
+$items = array_map(function($item) { return $item['text'];}, $menu_items);
+
+$key = $cache->generateKey($items);
+$menu = $cache->remember($key,
+function () use ($menu_items, $menu_id, $collapsed) {
+ $menu = new Navigation($menu_id);
+ $menu->asDrawer($collapsed)->populateFromArray($menu_items);
+ return $menu->render();
+});
+
+global $wp;
+
+$current = home_url($wp->request.'/');
+echo str_replace($current.'"', $current.'" class="current" aria-current="page"', $menu);
diff --git a/src/drawer-menu/save.js b/src/drawer-menu/save.js
new file mode 100644
index 0000000..8432b3a
--- /dev/null
+++ b/src/drawer-menu/save.js
@@ -0,0 +1,3 @@
+export default function save() {
+ return null; // Server-side rendered
+}
diff --git a/src/drawer-menu/style.scss b/src/drawer-menu/style.scss
new file mode 100644
index 0000000..52e11af
--- /dev/null
+++ b/src/drawer-menu/style.scss
@@ -0,0 +1,88 @@
+nav.drawer {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ max-height: 100vh;
+ overflow: hidden auto;
+ width: var(--btn);
+ z-index: var(--z-5);
+ transition: var(--trans-size);
+ --dir: column-reverse;
+ background-color: var(--base);
+ border-left: 1px solid var(--base-200);
+ box-shadow:rgba(var(--base-rgb), var(--op-4)) var(--shdw-left);
+ height: auto;
+ --w: var(--chip_);
+
+ .title,
+ .section-title {
+ display: none;
+ }
+ ul .icon {
+ min-width: var(--chip_);
+ }
+
+ a,
+ .a {
+ height: var(--chipchip);
+ padding: 0 .5rem;
+ width: 100%;
+ gap: .5rem;
+ justify-content: center;
+ }
+
+ .toggle {
+ width: 100%;
+ .icon {
+ transform: rotate(0);
+ transition: var(--trans-transform);
+ }
+ }
+ &.open {
+ width: 240px;
+ .title,
+ .section-title {
+ display: block;
+ }
+
+ .toggle .icon {
+ transform: rotate(180deg);
+ }
+
+ a,
+ .a {
+ justify-content: flex-start;
+ }
+ }
+
+ ul {
+ --dir: column;
+ --gap: 0;
+ --height: auto;
+ padding: 0;
+ margin: 0;
+ height: auto;
+ width:100%;
+ }
+ li {
+ height: auto;
+ width: 100%;
+ }
+
+
+ .row {
+ width: 100%;
+ }
+
+ .menu-section {
+ border-bottom: 1px solid var(--contrast-200);
+ }
+ .section-title {
+ padding: 0.5rem var(--px);
+ font-size: var(--small);
+ text-transform: uppercase;
+ opacity: 0.6;
+ font-weight: bold;
+ }
+
+}
diff --git a/src/drawer-menu/view.js b/src/drawer-menu/view.js
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/drawer-menu/view.js
@@ -0,0 +1 @@
+
diff --git a/src/feed/render.php b/src/feed/render.php
index 7af8d29..57c50e9 100644
--- a/src/feed/render.php
+++ b/src/feed/render.php
@@ -79,8 +79,7 @@
$type = 'term';
$context = 'taxonomy:'.$context;
}
- jvbDump($settings);
- jvbDump($contentTypes);
+
if (empty($contentTypes)) {
return;
}
diff --git a/src/feed/style.scss b/src/feed/style.scss
index 74fbf73..b126889 100644
--- a/src/feed/style.scss
+++ b/src/feed/style.scss
@@ -643,7 +643,7 @@
.feed-block {
grid-column: full;
- .feed-filters {
+ .filters {
padding: 1rem 0;
max-width:var(--wide);
margin: 0 auto;
@@ -690,6 +690,10 @@
}
+ h3 {
+ margin: 0 0 .25rem;
+ font-size: var(--medium);
+ }
}
/** PLACEHOLDERS **/
.placeholder {
@@ -721,20 +725,42 @@
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: fit-content;
padding: 0;
+ details {
+ z-index: var(--z-2);
+ width: 100%;
+ position: relative;
+ padding: 0;
+ summary {
+ position:absolute;
+ top: -3rem;
+ left:0;
+ width: 100%;
+ background-color: rgba(var(--base-rgb),var(--op-2));
+ backdrop-filter: blur(5px);
+ &:hover {
+ background-color: rgba(var(--action-rgb),var(--op-45));
+ }
+ }
+ &[open] {
+ padding: .25rem .5rem;
+ summary .icon {
+ opacity: 0;
+ }
+ }
+
+ }
img {
- opacity: .7;
- filter: grayscale(.5) sepia(.3) blur(7px);
- transition: opacity var(--trans-base), filter var(--trans-base);
- &[data-loaded=true] {
- opacity: 1;
- filter: none;
+ object-fit: cover;
+ //opacity: .7;
+ //filter: grayscale(.5) sepia(.3);
+ &:hover {
+ opacity: .8;
}
}
&[data-timeline] {
- aspect-ratio: unset;
- summary {
+ .images {
aspect-ratio: 3/2;
padding: 0 0 1rem;
span {
@@ -778,46 +804,6 @@
display: none;
}
}
- details a {
- font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
- }
-
- &.highlighted {
- box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1);
- animation: highlight-puls 2s ease-in-out;
- }
- &[open],
- &:hover {
- .handle {
- background-color: var(--overlay-pink-medium);
- backdrop-filter: blur(5px);
- }
- }
- summary {
- width: calc(100% - 1rem);
- height: 100%;
- aspect-ratio: 1;
- .handle {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background-color: rgba(var(--base-rgb),var(--op-3));
- backdrop-filter: blur(5px);
- border-radius: var(--radius);
- z-index: 1;
- padding: .25rem .25rem .25rem 1.1rem;
- }
- &::after {
- z-index: 11;
- position: absolute;
- bottom: .35rem;
- right: .7rem;
- width: 1.5rem;
- height: 1.5rem;
- cursor: pointer;
- }
- }
label {
font-weight: normal;
diff --git a/src/feed/view.js b/src/feed/view.js
index 710dd6c..fdec017 100644
--- a/src/feed/view.js
+++ b/src/feed/view.js
@@ -63,15 +63,17 @@
};
this.ui = window.uiFromSelectors(this.elements);
- this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]');
+
+ this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
- if (this.ui.content.length > 0) {
+ if (this.ui.content && this.ui.content.length > 0) {
this.contentTypes = Array.from(
this.ui.content
).map(content => content.value);
} else {
this.contentTypes = [this.container.dataset['content']];
}
+
if (this.ui.taxonomies.length>0) {
this.taxonomies = Array.from(
this.ui.taxonomies,
@@ -79,6 +81,8 @@
} else {
this.taxonomies = [];
}
+
+
}
async initTaxonomies() {
@@ -176,13 +180,15 @@
this.syncUIToFilters();
}
syncUIToFilters() {
- // Check radio buttons
- Object.entries(this.filters).forEach(([key, value]) => {
- const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
- if (input) {
- input.checked = true;
- }
- });
+ if (this.ui.filterContainer) {
+ // Check radio buttons
+ Object.entries(this.filters).forEach(([key, value]) => {
+ const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+ if (input) {
+ input.checked = true;
+ }
+ });
+ }
// Update content-specific visibility
this.updateContentFor(this.filters.content);
@@ -282,7 +288,7 @@
this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
}
});
- if (hasTaxonomy) {
+ if (this.ui.filterContainer && hasTaxonomy) {
for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
if (button) {
@@ -359,9 +365,6 @@
this.removePlaceholders();
this.ui.grid.append(fragment);
- // Observe images after adding to DOM
- this.observeImages(this.ui.grid);
-
if (this.config.gallery) {
this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
}
@@ -377,8 +380,12 @@
this.a11y.announceItems(0, this.store.filters['page'] >1, false);
}
- this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
- this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ if (this.ui.filters.match) {
+ this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ if (this.ui.clearFilter) {
+ this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
}
/**
@@ -386,13 +393,50 @@
* @param {object} item
*/
createItemElement(item) {
- let template = window.getTemplate('feed-item');
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
if (Object.hasOwn(template.dataset, 'timeline')) {
return this.createTimelineElement(item, template);
}
+ //fields
+ for (let [fieldName, value] of Object.entries(item.fields)) {
+ let el = template.querySelector(`[data-field="${fieldName}"]`);
+ if (!el) {
+ continue;
+ }
+ if (Object.keys(item.images).map((img)=> parseInt(img)).includes(value)) {
+ [
+ el.src,
+ el.srcset,
+ el.alt
+ ] = [
+ item.images[value].tiny,
+ `${item.images[value].tiny} 50w, ${item.images[value].small} 300w, ${item.images[value].medium} 1024w`,
+ item.images[value]['image-alt-text']
+ ];
+ } else if (el.tagName === 'TIME') {
+ el.setAttribute('datetime', value);
+ el.textContent = window.formatTimeAgo(value);
+ } else {
+ el.textContent = value;
+ }
+ if (value === '') {
+ el.remove();
+ }
+ }
+ let link = template.querySelector('a');
+ if (link && item.url !== '') {
+ [
+ link.href,
+ link.title
+ ] = [
+ item.url,
+ `View ${item.fields['post_title']??'Item'}`
+ ];
+ }
return template;
}
+
createTimelineElement(item, template) {
let [
main,
@@ -467,7 +511,7 @@
let rand = Math.floor(Math.random() * total);
let icon;
- if (this.ui.content.length > 0) {
+ if (this.ui.content && this.ui.content.length > 0) {
icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
} else {
icon = window.getIcon(this.container.dataset.icon);
@@ -584,35 +628,6 @@
document.addEventListener('change', this.changeHandler);
}
- loadImage(img) {
- const src = this.getAppropriateImageSize(img);
- if (src && src !== img.src) {
- img.src = src;
- img.dataset.loaded = 'true';
- }
- }
-
- getAppropriateImageSize(img) {
- const width = window.innerWidth;
-
- if (width < 768 && img.dataset.small) {
- return img.dataset.small;
- } else if (img.dataset.medium) {
- return img.dataset.medium;
- }
-
- return img.src; // Fallback to current src
- }
-
- observeImages(container) {
- const images = container.querySelectorAll('img[data-small], img[data-medium]');
- images.forEach(img => {
- if (!img.dataset.loaded) {
- this.imageObserver.observe(img);
- }
- });
- }
-
handlePopState(e) {
if (e.state?.filters) {
if (this.processURLFilters()) {
@@ -688,4 +703,32 @@
}
});
+ let item = {
+ content: "art",
+ date: "2025-12-24 03:37:26",
+ fields: {
+ gallery: "",
+ post_content: "",
+ post_thumbnail: 200,
+ post_title: "Great Gray Owl",
+ price: "",
+ },
+ icon: "arrows-clockwise",
+ id: 195,
+ images: {
+ 200: {
+ 'image-alt-text': "",
+ 'image-caption': "",
+ 'image-title': "Great Gray Owl",
+ large: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl.jpg",
+ medium: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-1024x1024.jpg",
+ small: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-300x300.jpg",
+ tiny: "http://jakevan.test/wp-content/uploads/2025/12/Great-Gray-Owl-50x50.jpg"
+ }
+ },
+ url: "http://jakevan.test/art/great-gray-owl/",
+ user_id: 3
+ };
});
+
+
diff --git a/src/forms/view.js b/src/forms/view.js
index 957a5df..20b1f71 100644
--- a/src/forms/view.js
+++ b/src/forms/view.js
@@ -3,12 +3,19 @@
* Frontend JavaScript for the Form Block
* Handles form validation and submission
*/
+/**
+ * view.js
+ * Frontend JavaScript for the Form Block
+ */
class FormBlock {
constructor() {
this.controller = new window.jvbForm();
document.querySelectorAll('.jvb-form-block form').forEach(form => {
- this.controller.registerForm(form, {autosave: true,autoUpload: false});
+ this.controller.registerForm(form, {
+ autosave: true,
+ autoUpload: false
+ });
});
this.controller.subscribe((event, data) => {
@@ -19,68 +26,57 @@
}
async handleFormSubmission(data) {
- let formId = data.formId;
- let formConfig = data.config;
- let formData = data.fullData;
- let form = formConfig.element;
+ const { formId, config: formConfig, fullData: formData } = data;
+ const form = formConfig.element;
- // Create FormData instead of JSON
const submitData = new FormData();
- // Add all regular form fields
+ // Add regular form fields
for (const [key, value] of Object.entries(formData)) {
- // Skip the nonce field - we don't need it for REST API with __return_true
- if (key === '_wpnonce' || key === '_wp_http_referer') {
- continue;
- }
+ if (key === '_wpnonce' || key === '_wp_http_referer') continue;
+
if (Array.isArray(value)) {
value.forEach(v => submitData.append(`${key}[]`, v));
} else if (typeof value === 'object' && value !== null) {
- // For nested objects, you might want to JSON.stringify them
submitData.append(key, JSON.stringify(value));
} else {
submitData.append(key, value);
}
}
- // Get uploaded files from UploadManager
+ // Add uploaded files
if (window.jvbUploads) {
- const files = await window.jvbUploads.getFilesForForm(form);
-
- // Append files with their field names
- files.forEach(({file, fieldName, uploadId, meta}) => {
- // Use fieldName[] for multiple files per field
- submitData.append(`${fieldName}[]`, file);
- });
+ try {
+ const files = await window.jvbUploads.getFilesForForm(form);
+ files.forEach(({ file, fieldName }) => {
+ submitData.append(`${fieldName}[]`, file);
+ });
+ } catch (error) {
+ console.error('Error getting files:', error);
+ }
}
-
- let headers = {
- // 'X-WP-Nonce': window.auth.getNonce()
- };
-
- let block = form.closest('.jvb-form-block');
this.controller.showFormStatus(formId, 'uploading');
try {
const response = await fetch(`${jvbSettings.api}forms`, {
method: 'POST',
credentials: 'same-origin',
- headers,
- body: submitData // Send FormData instead of JSON
+ body: submitData
});
+
const result = await response.json();
if (!response.ok) {
this.controller.showFormStatus(formId, 'error');
- this.controller.handleFormError(form, result); // ✓ Show error to user
- return; // Stop processing
+ this.controller.handleFormError(form, result);
+ return;
}
this.controller.showFormStatus(formId, 'submitted');
this.controller.showSummary(formId, '.jvb-form-block');
- // Clean up uploaded files from IndexedDB after successful submission
+ // Clean up uploaded files
if (window.jvbUploads) {
const uploadFields = form.querySelectorAll('[data-upload-field]');
for (const field of uploadFields) {
@@ -88,17 +84,16 @@
await window.jvbUploads.clearFieldFromStores(fieldId);
}
}
+
} catch (error) {
console.error('Form submission error:', error);
this.controller.showFormStatus(formId, 'error');
-
- // Show user-friendly network error - IMPROVED
this.controller.handleFormError(form, {
message: 'Network error. Please check your connection and try again.',
code: 'network_error'
});
} finally {
- this.controller.store.delete(formId);
+ await this.controller.store.delete(formId);
}
}
}
diff --git a/src/list/render.php b/src/list/render.php
index 2b56c65..f8ef568 100644
--- a/src/list/render.php
+++ b/src/list/render.php
@@ -169,7 +169,7 @@
$out .= '</section>';
echo $out;
- echo jvbBuildDirectoryNavigation();
+ echo JVB()->directories()?->renderNavigation();
return ob_get_clean();
}
diff --git a/src/summary/render.php b/src/summary/render.php
index d6a2040..4c22c7a 100644
--- a/src/summary/render.php
+++ b/src/summary/render.php
@@ -44,7 +44,7 @@
$artist = jvbContentFromUser((int)$current->post_author);
$sections = JVB_CONTENT[jvbNoBase($current->post_type)]['sections']??[];
- jvbDump($sections);
+
// $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
diff --git a/wp-config.php b/wp-config.php
new file mode 100644
index 0000000..5fe4c60
--- /dev/null
+++ b/wp-config.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * The base configuration for WordPress
+ *
+ * The wp-config.php creation script uses this file during the installation.
+ * You don't have to use the web site, you can copy this file to "wp-config.php"
+ * and fill in the values.
+ *
+ * This file contains the following configurations:
+ *
+ * * Database settings
+ * * Secret keys
+ * * Database table prefix
+ * * Localized language
+ * * ABSPATH
+ *
+ * @link https://wordpress.org/support/article/editing-wp-config-php/
+ *
+ * @package WordPress
+ */
+
+// ** Database settings - You can get this info from your web host ** //
+/** The name of the database for WordPress */
+define( 'DB_NAME', 'wp_9090417' );
+
+/** Database username */
+define( 'DB_USER', 'user-8484095' );
+
+/** Database password */
+define( 'DB_PASSWORD', 'CZvHXJ0wd2' );
+
+/** Database hostname */
+define( 'DB_HOST', '127.0.0.1' );
+
+/** Database charset to use in creating database tables. */
+define( 'DB_CHARSET', 'utf8' );
+
+/** The database collate type. Don't change this if in doubt. */
+define( 'DB_COLLATE', '' );
+
+/**#@+
+ * Authentication unique keys and salts.
+ *
+ * Change these to different unique phrases! You can generate these using
+ * the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
+ *
+ * You can change these at any point in time to invalidate all existing cookies.
+ * This will force all users to have to log in again.
+ *
+ * @since 2.6.0
+ */
+define( 'AUTH_KEY', 'qxPk61Jf2tn|mz0Dx$9(ikHT4)v[[uagWf3T3`x.Ds2NU[0y7*C3)Ci=ex+A~kp^' );
+define( 'SECURE_AUTH_KEY', 'kK7n[x_}ClcUw2^ow~%WR9aP.!O}kq7FXxbKuAwtBg@.2=r1~!x<R12Y_g10[P^{' );
+define( 'LOGGED_IN_KEY', '-0tgnq P _y8~m95C9E.[S+o]Z-GC/(|wyyn/}yO5 de51AM#)y#$i4=OQ!Q(2k=' );
+define( 'NONCE_KEY', '?V-na8MDy-n`S2>3<[i.D#27H/Qlt|v{=R/dSC}Km%p}r){aE?LB=-k:v2ip2#r)' );
+define( 'AUTH_SALT', '4L0JSr]Z!27na~D?P];2TEz*BVVG~jG;1Od|r28H;$CgWP:J~NUx>$ng.gke2s<5' );
+define( 'SECURE_AUTH_SALT', '8{LtR_$P;gi=c7j!w+>Pt6KS(T-]F2Wlf~Ca~4-_}A8=~]k3~6):06$;leK_?a G' );
+define( 'LOGGED_IN_SALT', 'hp6]B8`a? w^):H+|c!`UR[T5$rDjHQ$~X)^lZ~N>|swQ8h.1,c:Ut^K|~Rhe-kA' );
+define( 'NONCE_SALT', 'MnNfXD+<Ibe0C&<u]3xmawWV4~>#ckfXV#9@t<;gqr8>o$]D}NQZ6dXTY.L$by,G' );
+define( 'WP_CACHE_KEY_SALT', 'pU95(>.]6vpDh~9!F1C`T/ 02JB[EEMj*)W_pNDD{PRzEm`Oc+5iu?q6}9;F2[ F' );
+
+
+/**#@-*/
+
+/**
+ * WordPress database table prefix.
+ *
+ * You can have multiple installations in one database if you give each
+ * a unique prefix. Only numbers, letters, and underscores please!
+ */
+$table_prefix = 'wp_';
+define( 'WP_AUTO_UPDATE_CORE', false );
+
+
+/* Add any custom values between this line and the "stop editing" line. */
+// Redis Object Cache Configuration
+define('WP_REDIS_HOST', '10.3.2.169');
+define('WP_REDIS_PORT', 6379);
+define('WP_REDIS_PASSWORD', 'CFKceq00762');
+define('WP_REDIS_TIMEOUT', 1);
+define('WP_REDIS_READ_TIMEOUT', 1);
+
+define('WP_REDIS_DATABASE', 0);
+define('WP_REDIS_PREFIX', 'jakevan_');
+define('WP_REDIS_MAXTTL', 86400 * 7);
+
+
+/**
+ * For developers: WordPress debugging mode.
+ *
+ * Change this to true to enable the display of notices during development.
+ * It is strongly recommended that plugin and theme developers use WP_DEBUG
+ * in their development environments.
+ *
+ * For information on other constants that can be used for debugging,
+ * visit the documentation.
+ *
+ * @link https://wordpress.org/support/article/debugging-in-wordpress/
+ */
+define( 'WP_DEBUG', true );
+define( 'WP_DEBUG_DISPLAY', true );
+@ini_set( 'display_errors', 1 );
+
+// Use dev versions of core JS and CSS files (only needed if you are modifying these core files)
+define('SCRIPT_DEBUG', true);
+
+
+
+define( 'WP_ALLOW_MULTISITE', true );
+define( 'MULTISITE', true );
+define( 'SUBDOMAIN_INSTALL', true );
+$base = '/';
+define( 'DOMAIN_CURRENT_SITE', 'jakevan.ca' );
+define( 'PATH_CURRENT_SITE', '/' );
+define( 'SITE_ID_CURRENT_SITE', 1 );
+define( 'BLOG_ID_CURRENT_SITE', 1 );
+
+/* That's all, stop editing! Happy publishing. */
+
+/** Absolute path to the WordPress directory. */
+if ( ! defined( 'ABSPATH' ) ) {
+ define( 'ABSPATH', __DIR__ . '/' );
+}
+
+/** Sets up WordPress vars and included files. */
+require_once ABSPATH . 'wp-jelastic.php';
+require_once ABSPATH . 'wp-settings.php';
+
--
Gitblit v1.10.0