From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start
---
src/faq/style.scss | 72
src/drawer-menu/index.php | 0
src/feed/index.js | 39
src/fields/view.js | 1
src/summary/render.php | 320 +
inc/blocks/SummaryBlock.php | 2
jvb.php | 7
inc/admin/ContentTaxonomy.php | 2
inc/meta/Form.php | 45
src/feed/render.php | 4
inc/managers/DashboardManager.php | 8
src/feed/edit.js | 256
src/forms/style.scss | 5572 +++++++++++++++++++++
src/feed/index.php | 2
src/menu/index.php | 0
src/summary/editor.scss | 20
src/video/index.js | 21
inc/ui/Checkout.php | 4
inc/ui/Tabs.php | 2
inc/helpers/crud.php | 2
build/gmbreviews/render.php | 4
src/faq/render.php | 0
build/summary/render.php | 4
inc/managers/LoginManager.php | 8
src/video/render.php | 1
src/drawer-menu/index.js | 10
src/faq/index.js | 11
inc/ui/CRUDSkeleton.php | 28
inc/integrations/Integrations.php | 4
src/fields/edit.js | 29
src/fields/index.php | 0
src/forms/render.php | 55
src/timeline/edit.js | 38
src/feed/editor.scss | 128
src/feed/style.scss | 956 +++
src/faq/index.php | 0
src/drawer-menu/save.js | 3
src/forms/view.js | 112
src/timeline/view.js | 0
src/glossary/style.scss | 109
src/feed/view.js | 747 ++
src/timeline/editor.scss | 0
src/video/block.json | 79
src/index.php | 3
src/glossary/edit.js | 38
src/gmbreviews/block.json | 68
src/gmbreviews/edit.js | 69
src/gmbreviews/style.scss | 122
src/glossary/index.php | 0
src/glossary/editor.scss | 0
src/glossary/index.js | 33
src/glossary/view.js | 184
src/menu/style.scss | 0
src/glossary/block.json | 24
src/video/index.php | 0
src/gmbreviews/index.php | 0
src/forms/edit.js | 319 +
src/forms/editor.scss | 0
src/timeline/index.js | 33
src/summary/style.scss | 20
src/fields/block.json | 25
src/gmbreviews/index.js | 11
src/menu/index.js | 33
inc/utility/Image.php | 6
src/menu/editor.scss | 0
src/gmbreviews/view.js | 0
webpack.jvb.js | 4
src/forms/save.js | 23
src/drawer-menu/render.php | 38
build/feed/view.js | 2
src/fields/editor.scss | 20
src/menu/block.json | 24
inc/users/UserSettings.php | 2
src/feed/block.json | 57
src/fields/save.js | 3
assets/js/concise/navigation.js | 2
assets/js/min/navigation.min.js | 2
src/feed/save.js | 3
src/faq/block.json | 34
inc/helpers/ui.php | 26
inc/forms/TaxonomySelector.php | 10
assets/js/concise/UtilityFunctions.js | 63
inc/blocks/CustomBlocks.php | 2639 +++++++--
assets/js/min/utility.min.js | 2
src/faq/edit.js | 145
src/forms/index.js | 40
src/timeline/style.scss | 135
src/timeline/render.php | 8
src/faq/view.js | 84
src/summary/index.php | 0
inc/managers/queue/executors/ContentExecutor.php | 4
templates/dashboard/sections/notifications.php | 2
inc/managers/IconsManager.php | 4
src/summary/index.js | 39
src/drawer-menu/style.scss | 88
src/timeline/index.php | 0
src/summary/block.json | 32
src/gmbreviews/render.php | 207
inc/managers/DirectoryManager.php | 10
inc/blocks/FeedBlock.php | 12
inc/managers/SEO/BreadcrumbManager.php | 15
src/forms/index.php | 0
src/video/editor.scss | 141
inc/helpers/renderFields.php | 4
src/summary/save.js | 3
src/fields/index.js | 39
src/video/style.scss | 178
src/summary/edit.js | 29
src/fields/style.scss | 20
src/video/edit.js | 276 +
src/video/view.js | 47
inc/helpers/media.php | 2
src/video/save.js | 23
inc/managers/ReferralManager.php | 16
src/menu/edit.js | 38
inc/blocks/MenuBlock.php | 4
src/menu/render.php | 8
assets/css/nav.min.css | 2
src/drawer-menu/block.json | 27
src/gmbreviews/editor.scss | 0
src/menu/view.js | 43
src/fields/render.php | 320 +
src/drawer-menu/view.js | 1
src/forms/block.json | 47
build/feed/view.asset.php | 2
src/summary/view.js | 1
src/faq/editor.scss | 99
src/drawer-menu/edit.js | 33
templates/dashboard/sections/news.php | 6
src/timeline/block.json | 23
src/feed/viewOld.js | 770 ++
src/drawer-menu/editor.scss | 0
build/fields/render.php | 4
inc/templates.php | 6
inc/blocks/FormBlock.php | 4
src/glossary/render.php | 8
136 files changed, 14,874 insertions(+), 822 deletions(-)
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index 65848db..8e31743 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;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{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%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.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:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px 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>.toggle{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>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}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-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.always.fixed>ul{padding-top:var(--btn)}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@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:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);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{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height: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)}.wp-site-blocks>header,body>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)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}
\ No newline at end of file
+nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul,ul.socials{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%;padding:0;margin:0}nav.col,nav.col ul{height:max-content}nav>ul{width:100%;overflow:auto hidden}nav li{width:max-content;--justify:center;max-inline-size:none;padding:0;list-style:none}nav.fill li{width:100%}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.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:1px solid var(--action-0);outline-offset:1px}nav.always{overflow:visible;transition:width var(--trans-base);width:max-content}nav.always>ul{--dir:column;--align:center;--justify:flex-end;--gap:0;height:100vh;max-height:none;position:fixed;right:-300vw;bottom:0;width:100vw;padding:var(--btn) 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{width:100%}nav.always.fixed{width:var(--btn);height:var(--btn);bottom:0;right:0;overflow:hidden}nav.always.fixed .toggle.main{background-color:var(--base)}nav.always.fixed .toggle.main:focus,nav.always.fixed .toggle.main:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.mobile .toggle.main{width:var(--btn);transition:width var(--trans-base)}nav.mobile .icon-list,nav.mobile .icon-x{--w:32px}nav.mobile .icon-x,nav.mobile.open .icon-list{display:none}nav.mobile .icon-list,nav.mobile.open .icon-x{display:block}nav.mobile.open>ul{--dir:column;z-index:var(--z-9);background-color:rgba(var(--base-rgb),var(--op-6));width:100vw;height:100vh;overflow:hidden auto;right:0;bottom:0;position:fixed;padding:var(--btn) 0}nav.always>ul::before,nav.mobile.open>ul::before{content:'';z-index:-1;position:absolute;inset:0;filter:blur(5px)}nav.always.open .main.toggle,nav.mobile.open .main.toggle{position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-10);aspect-ratio:unset}nav.always>ul,nav.always>ul:before,nav.mobile.open>ul,nav.mobile.open>ul::before{background-color:rgba(var(--base-rgb),var(--op-6))}@media (max-width:767px){nav.col{height:var(--btn)}nav.mobile>ul{--dir:column;--align:center;--justify:flex-end;--gap:0;height:100%;max-height:none;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto}nav.mobile.open>ul{right:0}}@media (min-width:768px){nav.mobile:not(.always) .toggle.main{display:none}}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-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{left:0;bottom:0;width:calc(100% - var(--btn))}nav.fixed:not(.always) ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed:not(.always) li{flex:1}nav.fixed a{--align:center;--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@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:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}.condensed ul{--justify:center;--dir:row}nav.condensed{--gap:0 .25rem;width:100%;--justify:center}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:flex-end;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x;width:100%}ul.socials li{list-style:none}.always ul.socials{width:100vw;--justify:stretch}.always ul.socials li{flex:1;--justify:center;--align:center}.always ul.socials a{display:inline-flex}ul.socials a{display:inline-block;font-size:var(--txt-x-small);padding:.25rem .5rem;max-width:none}ul.socials .icon{margin:0}ul.socials .icon+span:not(.screen-reader-text){margin-left:.5rem}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height: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)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:var(--align,center);justify-content:var(--justify,space-between);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)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}nav.pagination{width:100%}nav.pagination>a{min-width:var(--chipchip)}nav.pagination>ul{margin:0 auto}nav.pagination:not(:has(a+ul)){margin-left:var(--chipchip)}nav.pagination:not(:has(ul+a)){margin-right:var(--chipchip)}.pagination.condensed li+li::before{display:none}.pagination li.current{width:var(--chip_);height:var(--chip_);background-color:var(--action-0);border-radius:var(--radius);line-height:1}.pagination.condensed a{font-size:var(--txt-medium);width:var(--chip_);height:var(--chip_)}
\ No newline at end of file
diff --git a/assets/js/concise/UtilityFunctions.js b/assets/js/concise/UtilityFunctions.js
index ca5944e..d11cdbc 100644
--- a/assets/js/concise/UtilityFunctions.js
+++ b/assets/js/concise/UtilityFunctions.js
@@ -992,12 +992,75 @@
{ passive: true }
);
+
+window.previousBGSize = 'Small';
+window.bgSizes = {
+ Small: 500,
+ Med: 768,
+ Large: 1024
+};
+
+window.bgObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ let newSize = entry.target.dataset[`bg${window.previousBGSize}`];
+ entry.target.style.backgroundImage = `url(${newSize})`;
+ entry.target.dataset.bgImg = window.previousBGSize;
+ window.bgObserver.unobserve(entry.target);
+ }
+ })
+ },
+ {
+ root: null,
+ rootMargin: '0px 0px -100px 0px',
+ threshold: 0
+ });
+
+function updateBG() {
+ let current = window.innerWidth;
+ let newWidth = getBGWidth(current);
+
+ if (newWidth) {
+ window.previousBGSize = newWidth;
+ document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(img => {
+ window.bgObserver.observe(img);
+ });
+ }
+
+}
+function getBGWidth(width) {
+ let prev = window.previousBGSize;
+
+ let check = {
+ Small: ['Med','Large'],
+ Med: ['Large'],
+ Large: false
+ };
+
+ if (!check[prev]) {
+ return false;
+ }
+ let next = 'Small';
+
+ check[prev].forEach(w => {
+ if (width => window.bgSizes[w]) {
+ next = w;
+ }
+ });
+ return next;
+}
+
+updateBG();
+
// Debounced resize to recalc scrollable height
window.addEventListener('resize', () => {
window.debouncer.schedule('recalc-max-scroll', () => {
updateMaxScroll();
updateScrollProgress(window.scrollY || docEl.scrollTop || 0);
}, 20);
+ window.debouncer.schedule('bg-resize', () => {
+ updateBG();
+ });
});
// Initial setup
diff --git a/assets/js/concise/navigation.js b/assets/js/concise/navigation.js
index 062ea24..10026f9 100644
--- a/assets/js/concise/navigation.js
+++ b/assets/js/concise/navigation.js
@@ -17,7 +17,7 @@
this.navs = new Map();
document.querySelectorAll('nav:has(.submenu), nav:has(.toggle)').forEach(nav => {
let navID = nav.id;
- if (navID === '') {
+ if (navID === '' || this.navs.has(navID)) {
navID = `nav-${this.counter}`;
nav.id = navID;
this.counter++;
diff --git a/assets/js/min/navigation.min.js b/assets/js/min/navigation.min.js
index 308523e..d99198a 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.openSubmenu=null,this.releaseFocusTrap=null,this.clicked=new Set,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.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)));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.counter++,n.forEach(e=>{e.id="submenu-"+this.counter,e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)),this.counter++}),this.navs.set(t,a)})}initListeners(){this.clickListener=this.handleClick.bind(this),this.keysListener=this.handleKeys.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;let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav"),s=!this.clicked.has(e);return e.classList.contains("open")!==s&&this.toggleNav(s,e.id),void(s?this.clicked.add(e):this.clicked.delete(e))}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li"),t=!this.clicked.has(e),n=e.classList.contains("open")!==t;return t?this.clicked.add(e):this.clicked.delete(e),void(n&&this.toggleSubmenu(t,e))}if(!this.openNav)return;let n=!0;for(let[t,s]of this.navs)if(e.target.closest("#"+t)){n=!1;break}n&&this.toggleNav(!1,this.openNav)}handleHoverOn(e){let t=e.currentTarget;this.clicked.has(t)||t.closest("nav.sidebar")||(t.classList.contains("has-submenu")?this.toggleSubmenu(!0,t):"NAV"===t.tagName&&(t.classList.contains("mobile")||this.toggleNav(!0,t.id)))}handleHoverOff(e){let t=e.currentTarget;if(!this.clicked.has(t)&&!t.closest("nav.sidebar"))if(t.classList.contains("has-submenu"))this.toggleSubmenu(!1,t);else if("NAV"===t.tagName){if(t.classList.contains("mobile"))return;let e=this.navs.get(t.id),s=!0;for(let t of e.submenus)if(this.clicked.has(t)){s=!1;break}s&&this.toggleNav(!1,t.id)}}handleKeys(e){if(this.openNav)switch(e.key){case"Escape":this.closeAll();break;case"ArrowDown":this.focusNextItem(),e.preventDefault();break;case"ArrowUp":this.focusPrevItem(),e.preventDefault()}}closeAll(){let e=this.navs.get(this.openNav);e&&this.clicked.has(e.nav)&&this.clicked.delete(e.nav),this.toggleNav(!1,this.openNav)}focusNextItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t+1]||e[0]).focus()}focusPrevItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t-1]||e[e.length-1]).focus()}getFocusableItems(){return Array.from(document.querySelectorAll("nav.open a, nav.open button")).filter(e=>!e.disabled&&!e.closest("[hidden]")&&!e.closest("[inert]"))}toggleNav(e,t){let s=this.navs.get(t);if(s){if(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.keysListener),s.nav.classList.contains("mobile")&&(this.releaseFocusTrap=window.jvbA11y.trapFocus(s.nav))):(this.releaseFocusTrap&&(this.releaseFocusTrap(),this.releaseFocusTrap=null),this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.keysListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach(e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}),Array.from(s.submenus).forEach(e=>{this.clicked.has(e)&&this.clicked.delete(e)})),s.nav.classList.toggle("open",e),s.nav.classList.contains("mobile")){const t=s.nav.querySelector(":scope > ul");t&&(t.inert=!e)}s.nav.setAttribute("aria-expanded",e),e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus()}}toggleSubmenu(e,t){e&&this.openSubmenu&&this.openSubmenu!==t&&!this.openSubmenu.contains(t)&&this.toggleSubmenu(!1,this.openSubmenu),e?this.openSubmenu=t:this.openSubmenu===t&&(this.openSubmenu=null,this.clicked.delete(t));let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];e||s.focus();const i=t.querySelector(":scope > ul");i&&(i.inert=!e);let a=s.getAttribute("aria-label");window.jvbA11y.announce(e?`${a} expanded`:`${a} collapsed`),t.classList.toggle("open",e),s.setAttribute("aria-expanded",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.openSubmenu=null,this.releaseFocusTrap=null,this.clicked=new Set,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach(e=>{let t=e.id;(""===t||this.navs.has(t))&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)));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.counter++,n.forEach(e=>{e.id="submenu-"+this.counter,e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)),this.counter++}),this.navs.set(t,a)})}initListeners(){this.clickListener=this.handleClick.bind(this),this.keysListener=this.handleKeys.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;let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav"),s=!this.clicked.has(e);return e.classList.contains("open")!==s&&this.toggleNav(s,e.id),void(s?this.clicked.add(e):this.clicked.delete(e))}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li"),t=!this.clicked.has(e),n=e.classList.contains("open")!==t;return t?this.clicked.add(e):this.clicked.delete(e),void(n&&this.toggleSubmenu(t,e))}if(!this.openNav)return;let n=!0;for(let[t,s]of this.navs)if(e.target.closest("#"+t)){n=!1;break}n&&this.toggleNav(!1,this.openNav)}handleHoverOn(e){let t=e.currentTarget;this.clicked.has(t)||t.closest("nav.sidebar")||(t.classList.contains("has-submenu")?this.toggleSubmenu(!0,t):"NAV"===t.tagName&&(t.classList.contains("mobile")||this.toggleNav(!0,t.id)))}handleHoverOff(e){let t=e.currentTarget;if(!this.clicked.has(t)&&!t.closest("nav.sidebar"))if(t.classList.contains("has-submenu"))this.toggleSubmenu(!1,t);else if("NAV"===t.tagName){if(t.classList.contains("mobile"))return;let e=this.navs.get(t.id),s=!0;for(let t of e.submenus)if(this.clicked.has(t)){s=!1;break}s&&this.toggleNav(!1,t.id)}}handleKeys(e){if(this.openNav)switch(e.key){case"Escape":this.closeAll();break;case"ArrowDown":this.focusNextItem(),e.preventDefault();break;case"ArrowUp":this.focusPrevItem(),e.preventDefault()}}closeAll(){let e=this.navs.get(this.openNav);e&&this.clicked.has(e.nav)&&this.clicked.delete(e.nav),this.toggleNav(!1,this.openNav)}focusNextItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t+1]||e[0]).focus()}focusPrevItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t-1]||e[e.length-1]).focus()}getFocusableItems(){return Array.from(document.querySelectorAll("nav.open a, nav.open button")).filter(e=>!e.disabled&&!e.closest("[hidden]")&&!e.closest("[inert]"))}toggleNav(e,t){let s=this.navs.get(t);if(s){if(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.keysListener),s.nav.classList.contains("mobile")&&(this.releaseFocusTrap=window.jvbA11y.trapFocus(s.nav))):(this.releaseFocusTrap&&(this.releaseFocusTrap(),this.releaseFocusTrap=null),this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.keysListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach(e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}),Array.from(s.submenus).forEach(e=>{this.clicked.has(e)&&this.clicked.delete(e)})),s.nav.classList.toggle("open",e),s.nav.classList.contains("mobile")){const t=s.nav.querySelector(":scope > ul");t&&(t.inert=!e)}s.nav.setAttribute("aria-expanded",e),e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus()}}toggleSubmenu(e,t){e&&this.openSubmenu&&this.openSubmenu!==t&&!this.openSubmenu.contains(t)&&this.toggleSubmenu(!1,this.openSubmenu),e?this.openSubmenu=t:this.openSubmenu===t&&(this.openSubmenu=null,this.clicked.delete(t));let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];e||s.focus();const i=t.querySelector(":scope > ul");i&&(i.inert=!e);let a=s.getAttribute("aria-label");window.jvbA11y.announce(e?`${a} expanded`:`${a} collapsed`),t.classList.toggle("open",e),s.setAttribute("aria-expanded",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/utility.min.js b/assets/js/min/utility.min.js
index 808a5c5..efbbfc4 100644
--- a/assets/js/min/utility.min.js
+++ b/assets/js/min/utility.min.js
@@ -1 +1 @@
-(()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,()=>{e.remove()},500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,t=>e[t])}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",()=>{window.loadTemplates()}),window.loadTemplates=function(){document.querySelectorAll("template").forEach(e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach(e=>{window.templates.has(e)||window.templates.set(e,n)})}})},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach(e=>{e.classList.forEach(t=>{this.templates.has(t)||this.templates.set(t,e)})})}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",()=>{window.jvbTemplates.registerAll()}),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);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(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout(()=>n=!1,t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach(e=>{const n=t(e);n&&i.append(n)}),n(i),await new Promise(e=>requestAnimationFrame(e))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,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(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise(i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval(()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())},n)})},window.eraseText=function(e,t=10){return new Promise(n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval(()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())},t)})},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise(e=>setTimeout(e,o)),s)&&(await window.eraseText(e,i),s);)await new Promise(e=>setTimeout(e,r))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,function(e){return e[1].toUpperCase()})},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise(t=>setTimeout(t,e))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",()=>this.cleanup())}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout(()=>{t(),this.timeouts.delete(e)},n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",()=>{o||(o=!0,requestAnimationFrame(l))},{passive:!0}),window.addEventListener("resize",()=>{window.debouncer.schedule("recalc-max-scroll",()=>{a(),s(window.scrollY||e.scrollTop||0)},20)}),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value},window.focusNextElement=function(){if(document.activeElement&&document.activeElement.form){var e=Array.prototype.filter.call(document.activeElement.form.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'),function(e){return e.offsetWidth>0||e.offsetHeight>0||e===document.activeElement}),t=e.indexOf(document.activeElement);if(t>-1)(e[t+1]||e[0]).focus()}}})();
\ No newline at end of file
+(()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,()=>{e.remove()},500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,t=>e[t])}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",()=>{window.loadTemplates()}),window.loadTemplates=function(){document.querySelectorAll("template").forEach(e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach(e=>{window.templates.has(e)||window.templates.set(e,n)})}})},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach(e=>{e.classList.forEach(t=>{this.templates.has(t)||this.templates.set(t,e)})})}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",()=>{window.jvbTemplates.registerAll()}),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);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(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout(()=>n=!1,t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach(e=>{const n=t(e);n&&i.append(n)}),n(i),await new Promise(e=>requestAnimationFrame(e))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,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(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise(i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval(()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())},n)})},window.eraseText=function(e,t=10){return new Promise(n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval(()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())},t)})},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise(e=>setTimeout(e,o)),s)&&(await window.eraseText(e,i),s);)await new Promise(e=>setTimeout(e,r))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,function(e){return e[1].toUpperCase()})},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise(t=>setTimeout(t,e))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",()=>this.cleanup())}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout(()=>{t(),this.timeouts.delete(e)},n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}function c(){window.innerWidth;let e=function(){let e=window.previousBGSize,t={Small:["Med","Large"],Med:["Large"],Large:!1};if(!t[e])return!1;let n="Small";return t[e].forEach(e=>{(t=>window.bgSizes[e])&&(n=e)}),n}();e&&(window.previousBGSize=e,document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(e=>{window.bgObserver.observe(e)}))}window.addEventListener("scroll",()=>{o||(o=!0,requestAnimationFrame(l))},{passive:!0}),window.previousBGSize="Small",window.bgSizes={Small:500,Med:768,Large:1024},window.bgObserver=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){let t=e.target.dataset[`bg${window.previousBGSize}`];e.target.style.backgroundImage=`url(${t})`,e.target.dataset.bgImg=window.previousBGSize,window.bgObserver.unobserve(e.target)}})},{root:null,rootMargin:"0px 0px -100px 0px",threshold:0}),c(),window.addEventListener("resize",()=>{window.debouncer.schedule("recalc-max-scroll",()=>{a(),s(window.scrollY||e.scrollTop||0)},20),window.debouncer.schedule("bg-resize",()=>{c()})}),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value},window.focusNextElement=function(){if(document.activeElement&&document.activeElement.form){var e=Array.prototype.filter.call(document.activeElement.form.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'),function(e){return e.offsetWidth>0||e.offsetHeight>0||e===document.activeElement}),t=e.indexOf(document.activeElement);if(t>-1)(e[t+1]||e[0]).focus()}}})();
\ No newline at end of file
diff --git a/build/feed/view.asset.php b/build/feed/view.asset.php
index c828fa8..0e01911 100644
--- a/build/feed/view.asset.php
+++ b/build/feed/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'c3aa1c027f932096017e');
+<?php return array('dependencies' => array(), 'version' => 'b9d79a303cb5aad9e29a');
diff --git a/build/feed/view.js b/build/feed/view.js
index a51b945..a5dae15 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.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".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"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(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.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){var t;return this.selector.getFieldId(null!==(t=Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0])&&void 0!==t?t:null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{var i;const s=null!==(i=t.dataset.for?.split(","))&&void 0!==i?i:[];t.hidden=s.length>0&&!s.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){var t;let i=null!==(t=this.ui.orderbyWrap.dataset.forOrder.split(","))&&void 0!==t?t:[];this.ui.orderbyWrap.hidden=!i.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}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.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=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:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{var i;"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=null===(i=!this.store.lastResponse?.has_more)||void 0===i||i))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=null!=e?e:this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{var i;this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),null!==(i=this.store.lastResponse?.has_more)&&void 0!==i&&i)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){var s;let r=null!==(s=i.images[t])&&void 0!==s&&s;r&&([e.src,e.srcset,e.alt]=[r.tiny,`${r.tiny} 50w, ${r.small} 300w, ${r.medium} 1024w`,r["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){var a;let r=null!==(a=t.taxonomies[i][s])&&void 0!==a&&a;if(!r)continue;let n=o.cloneNode(!0),l=n.querySelector("a");if(!l)continue;let h=window.decodeHTMLEntities(r.title);[l.href,l.title,l.textContent]=[r.url,`See more ${h}`,h],e.append(n)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}var a;i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${null!==(a=r.fields.post_title)&&void 0!==a?a:"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}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.error=window.jvbError,this.cache=new window.jvbCache("feed"),this.templates=window.jvbTemplates,this.config={source:"",context:"",highlight:null,gallery:!1,view:this.cache.get("feedView")||"grid",...this.container.dataset},this.init())}init(){this.initElements(),this.defineTemplates(),this.initListeners(),this.initFilters(),"requestIdleCallback"in window?requestIdleCallback(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},{timeout:2e3}):setTimeout(()=>{this.initStore(),this.initTaxonomies(),this.processCachedFilters(),this.processURLFilters(),this.updateFilterUI(),this.initGallery()},100)}initElements(){this.selectors={filterTrigger:"[data-filter]",filters:{actions:".filter-actions .toggle-text",container:".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"]'},grid:".item-grid",selected:".selected-items",buttons:{loadMore:"button.load-more",remove:".remove-term",clearFilters:"button.clear-filters",refresh:'button[data-action="refresh"]'}},this.ui=window.uiFromSelectors(this.selectors,this.container),this.ui.buttons.refresh=document.querySelector(this.selectors.buttons.refresh),this.ui.content=this.ui.filters.container.querySelectorAll('[name="content"]'),0===this.ui.content.length&&(this.ui.content=!1),this.ui.taxonomies=this.ui.filters.container.querySelectorAll("[data-taxonomy]"),0===this.ui.taxonomies.length&&(this.ui.taxonomies=!1),this.ui.orderbyWrap=this.ui.filters.container.querySelector("[data-for-order]"),0===this.ui.orderbyWrap.length&&(this.ui.orderbyWrap=!1),this.ui.order=this.ui.filters.container.querySelectorAll('[data-filter="order"]'),0===this.ui.order.length&&(this.ui.order=!1),this.ui.orderby=this.ui.filters.container.querySelectorAll('[data-filter="orderby"]'),0===this.ui.orderby.length&&(this.ui.orderby=!1),this.orderbyFilters=this.ui.orderby?Array.from(this.ui.orderby).map(e=>e.value):[],this.contentTypes=this.ui.content?Array.from(this.ui.content).map(e=>e.value):[this.container.dataset.content],this.taxonomies=this.ui.taxonomies?.length>0?Array.from(this.ui.taxonomies).map(e=>e.dataset.taxonomy):[]}initListeners(){this.popStateHandler=this.handlePopState.bind(this),this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),window.addEventListener("popstate",this.popStateHandler),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}initFilters(){this.allowedFilters=["content","order","orderby","favourites","match"];let e={content:this.contentTypes[0],orderby:"date",order:"desc",page:1};this.config.context&&(e.context=this.config.context),this.config.source&&(e.source=this.config.source),this.filters=e,this.defaults={...e}}updateFilterUI(){if(this.ui.filters.container&&([this.ui.content,this.ui.orderby,this.ui.order].forEach(e=>{if(e)for(let t of e){let[e,i]=[t.dataset.filter,t.value];if(!Object.hasOwn(this.store.filters,e))break;let s=this.store.filters[e]===i;if(s){t.checked=s;break}}}),Object.hasOwn(this.store.filters,"taxonomy")))for(let[e,t]of Object.entries(this.store.filters.taxonomy))t.forEach(e=>{e=parseInt(e),this.selector.store.get(e)&&this.createTermElement(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.selectors.buttons.loadMore)?this.nextPage():window.targetCheck(e,this.selectors.buttons.clearFilters)&&this.clearFilters();let t=window.targetCheck(e,this.selectors.buttons.remove);t&&this.removeSelectedTerm(t),window.targetCheck(e,this.selectors.buttons.refresh)&&(this.store.clearCache(),this.store.fetch());let i=window.targetCheck(e,'[data-filter="orderby"]');i&&"random"===i.value&&i.checked&&this.renderItems()}nextPage(){const e=(this.store.filters.page||1)+1,t=this.store.lastResponse?.pages||e;this.store.setFilters({page:Math.min(e,t)})}handleChange(e){const t=e.target;if(Object.hasOwn(t.dataset,"filter")){if(this.allowedFilters.includes(t.dataset.filter)){let e={};e[t.dataset.filter]=t.value,this.resetFilters(e)}switch(t.dataset.filter){case"content":this.updateContentFor(t.value);break;case"orderby":this.updateOrderOptions(t.value)}}}clearFilters(){this.taxFilters={},window.removeChildren(this.ui.selected),this.taxonomies.forEach(e=>{let t=this.getFieldId(e);this.selector.selectedTerms.get(t)?.clear()}),this.store.setFilters({...this.defaults,taxonomy:null}),this.updateURL(),this.saveToCacheFilters()}resetFilters(e){e={...this.store.filters,page:1,...e},this.store.setFilters(e),this.updateURL(),this.saveToCacheFilters()}getFieldId(e){return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(t=>t.dataset.taxonomy===e)[0]??null)}removeSelectedTerm(e){const t=parseInt(e.dataset.id),i=e.dataset.taxonomy;Object.hasOwn(this.taxFilters,i)&&(this.taxFilters[i]=this.taxFilters[i].filter(e=>e!==t),0===this.taxFilters[i].length&&delete this.taxFilters[i]),e.remove();const s=this.getFieldId(i);s&&(this.selector.activeField=s,this.selector.removeSelected(t,s)),this.resetFilters({taxonomy:Object.keys(this.taxFilters).length>0?this.taxFilters:null})}updateContentFor(e){[this.ui.taxonomies,this.ui.orderby].forEach(t=>{t&&t.forEach(t=>{const i=t.dataset.for?.split(",")??[];t.hidden=i.length>0&&!i.includes(e),t.hidden&&t.checked&&(t.checked=!1)})})}updateOrderOptions(e){if(this.ui.orderbyWrap){let t=this.ui.orderbyWrap.dataset.forOrder.split(",")??[];this.ui.orderbyWrap.hidden=!t.includes(e)}}updateFilterControls(){const e=0===Object.keys(this.taxFilters).length;this.ui.buttons.clearFilters&&(this.ui.buttons.clearFilters.hidden=e),this.ui.filters.actions&&(this.ui.filters.actions.hidden=e)}async initTaxonomies(){this.taxFilters={},this.selector=window.jvbSelector,this.selector.subscribe((e,t)=>{"selected-terms"===e&&this.handleTaxonomyChange(t)})}handleTaxonomyChange(e){const{terms:t,taxonomy:i}=e;0!==t.size&&(this.taxFilters[i]=Array.from(t),this.resetFilters({taxonomy:this.taxFilters}),t.forEach(e=>{this.createTermElement(e)}),this.updateFilterControls())}getTaxonomyIcon(e){let t=Array.from(this.ui.taxonomies).find(t=>t.dataset.taxonomy===e);return t?.dataset.icon.trim()||"tag"}createTermElement(e){const t=this.selector.store.get(e);t&&(this.ui.selected.querySelector(`[data-id="${e}"]`)||(t.icon=this.getTaxonomyIcon(t.taxonomy),this.ui.selected.append(this.templates.create("feedTerm",t))))}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.isFirstPage())return!1;const e=new URLSearchParams(window.location.search);if(!e.toString())return!1;let t=!1;this.allowedFilters.forEach(i=>{let s=e.get(`f_${i}`);s&&(t=!0,this.filters[i]=s)});let i=!1;return e.forEach((e,s)=>{if(s.startsWith("f_tax_")){i=!0,t=!0;const r=s.replace("f_tax_","");this.taxFilters[r]=e.split(",").map(Number)}}),t&&(i&&(this.filters.taxonomy=this.taxFilters),this.resetFilters(this.filters)),!0}updateURL(){const e=new URLSearchParams;this.allowedFilters.forEach(t=>{Object.hasOwn(this.store.filters,t)&&this.store.filters[t]!==this.defaults[t]&&e.set(`f_${t}`,this.store.filters[t])});for(let[t,i]of Object.entries(this.taxFilters))i.length>0&&e.set(`f_tax_${t}`,i.join(","));const t=`${window.location.pathname}${e.toString()?"?"+e.toString():""}`;t!==window.location.pathname+window.location.search&&window.history.pushState({filters:this.store.filters},"",t)}saveToCacheFilters(){Object.keys(this.store.filters).forEach(e=>{const t=`${this.config.source}_${this.config.context}_${e}`;this.store.filters[e]!==this.defaults[e]?this.cache.set(t,this.store.filters[e]):this.cache.remove(t)});const e=`${this.config.source}_${this.config.context}_taxonomy`;Object.keys(this.taxFilters).length>0?this.cache.set(e,this.taxFilters):this.cache.remove(e)}initGallery(){this.gallery=!!this.config.gallery&&window.jvbGallery,this.gallery&&this.gallery.subscribe((e,t)=>{"load-more"===e&&this.store.lastResponse?.has_more&&this.nextPage()})}initStore(){let e=this.orderbyFilters.filter(e=>!["date","modified","title","random"].includes(e)),t=[];e.forEach(e=>{t.push({name:e,keyPath:e})});const i=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:"date"},{name:"modified",keyPath:"modified"},{name:"title",keyPath:"title"},...t],filters:this.filters,TTL:216e5,showLoading:!0,required:"content"});this.store=i.feed,this.store.subscribe((e,t)=>{"data-loaded"===e&&(this.renderItems(t.items),this.ui.buttons.loadMore.hidden=!0,this.store.lastResponse&&this.store.lastResponse?.has_more&&(this.ui.buttons.loadMore.hidden=!this.store.lastResponse?.has_more??!0))})}isFirstPage(){return 1===this.store.filters.page}renderItems(e=null){e=e??this.store.getFiltered(),this.isFirstPage()&&window.removeChildren(this.ui.grid),0===e.length?(this.showEmptyState(),this.a11y.announceItems(0,this.isFirstPage())):window.chunkIt(e,e=>this.createItemElement(e),t=>{this.removePlaceholders(),this.ui.grid.append(t),this.config.gallery&&this.gallery.buildGalleryItems(".item img"),this.a11y.makeNavigable(this.ui.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,!this.isFirstPage(),this.store.lastResponse?.has_more??!1)},5).then(()=>{}),this.updateFilterControls()}showEmptyState(){window.removeChildren(this.ui.grid),this.ui.grid.append(this.templates.create("emptyState"))}createItemElement(e){if("object"==typeof e||(e=this.store.get(e)))return this.templates.create(`feedItem${window.uppercaseFirst(e.content)}`,e)}splitIDs(e){return String(e).split(",").map(e=>parseInt(e.trim())).filter(e=>e)}isImageField(e,t){return!(!Object.hasOwn(e,"images")||0===Object.keys(e.images).length)&&this.splitIDs(t).some(t=>Object.keys(e.images).map(e=>parseInt(e)).includes(parseInt(t)))}formatImageFields(e,t,i){let s=this.splitIDs(t);if(0!==s.length)if(s.length>1){let t=e.querySelector("img");if(!t)return;s.forEach(s=>{let r=t.cloneNode(!0);this.formatImageField(r,s,i),e.append(r)}),t.remove()}else{if("IMG"!==e.tagName&&!(e=e.querySelector("img")))return;this.formatImageField(e,s[0],i)}}formatImageField(e,t,i){let s=i.images[t]??!1;s&&([e.src,e.srcset,e.alt]=[s.tiny,`${s.tiny} 50w, ${s.small} 300w, ${s.medium} 1024w`,s["image-alt-text"]])}isTaxonomyField(e,t){return!(!Object.hasOwn(e,"taxonomies")||0===Object.keys(e.taxonomies).length)&&Object.keys(e.taxonomies).includes(t)}formatTaxonomyField(e,t,i,s){if("UL"!==e.tagName||!e.querySelector("li"))return;let r=this.splitIDs(s);0===r.length&&e.remove();let o=e.querySelector("li");for(let s of r){let r=t.taxonomies[i][s]??!1;if(!r)continue;let a=o.cloneNode(!0),n=a.querySelector("a");if(!n)continue;let l=window.decodeHTMLEntities(r.title);[n.href,n.title,n.textContent]=[r.url,`See more ${l}`,l],e.append(a)}o.remove()}isTimeField(e){return"TIME"===e.tagName||null!==e.querySelector("time")}formatTimeField(e,t){("TIME"===e.tagName||(e=e.querySelector("time")))&&(e.setAttribute("datetime",t),e.textContent=window.formatTimeAgo(t,"F Y"))}formatField(e,t){e.textContent=window.decodeHTMLEntities(t)}addTimelineElements(e,t){let[i,s,r,o]=[t.querySelector("span.after-text"),t.querySelector('[data-field="number"] b'),t.querySelector('[data-field="started"] time'),t.querySelector('[data-field="updated"] time')];i&&(i.textContent=`After ${e.number-1} Tx`),s&&(s.textContent=e.number-1),r&&this.formatTimeField(r,e.fields.timeline[0].post_date),o&&this.formatTimeField(o,e.fields.timeline[e.fields.timeline.length-1].post_date)}removePlaceholders(){const e=this.ui.grid.querySelectorAll(".placeholder");e.length>0&&e.forEach(e=>e.remove())}defineTemplates(){const e=this.templates,t=this;e.define("feedTerm",{refs:{icon:".icon",span:"span"},setup({el:e,refs:t,manyRefs:i,data:s}){e.dataset.id=s.id,e.dataset.taxonomy=s.taxonomy,t.icon&&(t.icon.className=`icon icon=${s.icon}`),t.span&&(t.span.textContent=window.decodeHTMLEntities(s.name))}}),e.define("emptyState"),this.contentTypes.forEach(i=>{e.define(`feedItem${window.uppercaseFirst(i)}`,{refs:{link:"a"},manyRefs:{fields:"[data-field]"},setup({el:e,refs:i,manyRefs:s,data:r}){const o=Object.hasOwn(e.dataset,"timeline");if(s.fields){for(let e of s.fields){if(o&&["timeline","number"].includes(e.dataset.field))continue;const i=!!Object.hasOwn(r.fields,e.dataset.field)&&r.fields[e.dataset.field];i?t.isImageField(r,i)?t.formatImageField(e,i,r):t.isTaxonomyField(r,e.dataset.field)?t.formatTaxonomyField(e,r,e.dataset.field,i):t.isTimeField(e)?t.formatTimeField(e,i):t.formatField(e,i):e.remove()}i.link&&""!==r.url&&(i.link.href=r.url,i.link.title=`View ${r.fields.post_title??"Item"}`),o&&t.addTimelineElements(r,e)}}})})}}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/fields/render.php b/build/fields/render.php
index 1c2b31a..b5645eb 100644
--- a/build/fields/render.php
+++ b/build/fields/render.php
@@ -120,7 +120,7 @@
</header>
<section>
<details class="bio-info">
- <summary class="row btw">
+ <summary class="row x-btw">
<h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2>
</summary>
<div class="columns stack-small">
@@ -242,7 +242,7 @@
</header>
<section>
<details class="bio-info">
- <summary class="row btw">
+ <summary class="row x-btw">
<h2>Learn More About <?=$current->name?></h2>
</summary>
<div class="map">
diff --git a/build/gmbreviews/render.php b/build/gmbreviews/render.php
index ee519f8..487f394 100644
--- a/build/gmbreviews/render.php
+++ b/build/gmbreviews/render.php
@@ -134,7 +134,7 @@
<?= apply_filters('wpautop', $comment) ?>
</div>
<?php } ?>
- <cite class="row start nowrap">
+ <cite class="row left nowrap">
<?php if (!empty($profilePhoto)) { ?>
<img src="<?=esc_url($profilePhoto)?>"
alt="<?=esc_attr($reviewer)?>"
@@ -145,7 +145,7 @@
</div>
<?php } ?>
- <div class="row start wrap">
+ <div class="row left wrap">
<?php if ($showRating && $rating > 0) { ?>
<div class="stars" title="<?= $rating ?> out of 5 stars">
<?php
diff --git a/build/summary/render.php b/build/summary/render.php
index 05636a1..c2c35fb 100644
--- a/build/summary/render.php
+++ b/build/summary/render.php
@@ -120,7 +120,7 @@
</header>
<section>
<details class="bio-info">
- <summary class="row btw">
+ <summary class="row x-btw">
<h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2>
</summary>
<div class="columns stack-small">
@@ -242,7 +242,7 @@
</header>
<section>
<details class="bio-info">
- <summary class="row btw">
+ <summary class="row x-btw">
<h2>Learn More About <?=$current->name?></h2>
</summary>
<div class="map">
diff --git a/inc/admin/ContentTaxonomy.php b/inc/admin/ContentTaxonomy.php
index 73476d4..399d3ac 100644
--- a/inc/admin/ContentTaxonomy.php
+++ b/inc/admin/ContentTaxonomy.php
@@ -290,7 +290,7 @@
<?php foreach ($results['details'] as $taxonomy => $detail): ?>
<details>
- <summary class="row btw">
+ <summary class="row x-btw">
<strong><?= esc_html(ucfirst($taxonomy)) ?></strong>
<?php if ($detail['errors'] === 0): ?>
<span style="color: green;">✓ Success</span>
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 0b16235..0b5830e 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -6,6 +6,7 @@
use JVBase\managers\Cache;
use JVBase\managers\LoginManager;
use JVBase\managers\SEO\BreadcrumbManager;
+use JVBase\utility\Image;
use WP_Block;
use WP_Query;
@@ -16,7 +17,16 @@
class CustomBlocks
{
protected Cache $cache;
- protected array $shouldRender = ['core/query'];
+
+ protected static ?WP_Query $currentLoop = null;
+ protected static ?int $currentQueryId = null;
+ protected static array $counters = [];
+ protected static ?WP_Query $originalQuery = null;
+ protected array $ignore = ['align','alt','area','backgroundColor','borderColor','buttonText','buttonPosition','buttonUseIcon','categories','className','columns','contentPosition','customOverlayColor','dimRatio','displayAsDropdown','displayAuthor','displayFeaturedImage','displayPostContent','displayPostContentRadio','displayPostDate','excerptLength','featuredImageAlign','fontSize','gradient','height','iconColor','iconColorValue','iconColorValue','iconBackgroundColor','iconBackgroundColorValue','id','imageFill','isDark','isLink','isSearchFieldHidden','isStackedOnMobile','isUserOverlayColor','kind','label','largestFontSize','layout','level','mediaId','mediaLink','mediaSizeSlug','mediaType','metadata','minHeight','minHeightUnit','opacity','opensInNewTab','order','orderBy','ordered','overlayMenu','placeholder','postLayout','postsToShow','query', 'queryId','ref','rel','shouldSyncIcon','showEmpty','showHierarchy','showLabel','showLabels','showOnlyTopLevel','showPostCounts','showTagCounts','size','sizeSlug','slug','smallestFontSize','tagName','taxonomy','term','textAlign','textColor','theme','title','type','url','useFeaturedImage','width','widthUnit',];
+
+ //For custom style output for nested links, etc
+ protected static array $pendingStyles = [];
+ protected static array $pendingClass = [];
public function __construct()
{
$this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
@@ -67,6 +77,13 @@
'label' => __('Callout Alt', 'jvb')
]
);
+ register_block_style(
+ 'core/separator',
+ [
+ 'name' =>'logo',
+ 'label' => __('With Logo', 'jvb')
+ ]
+ );
}
protected function checkMethods(?string $content, array $block, ?WP_Block $parent = null, bool $isPrerender = false):?string
{
@@ -79,7 +96,8 @@
if (function_exists($function)) {
return $function($block, $content, $parent);
} else if (method_exists($this, $method)) {
- return $this->$method($block, $content, $parent);
+ $content = $this->$method($block, $content, $parent);
+ return $isPrerender ? $this->maybeOutputCustomStyles().$content : $content;
} elseif (!empty($blockName) && JVB_TESTING) {
if (!in_array($block['blockName'], $this->getIgnore($isPrerender))) {
jvbDump('No method found for '.print_r($block['blockName'], true));
@@ -95,7 +113,13 @@
];
if ($isPrerender) {
$base = array_merge($base, [
-
+ 'core/query-pagination',
+ 'core/query-pagination-previous',
+ 'core/query-pagination-next',
+ 'core/query-pagination-numbers',
+ 'core/query',
+ 'core/calendar',
+ 'core/archives',
]);
} else {
$base = array_merge($base, [
@@ -151,10 +175,10 @@
}
$icon = '';
if (str_contains($url[1], 'google.com/maps')) {
- $icon = 'google-logo';
+ $icon = jvbIcon('google-logo');
}
if (str_contains($url[1], 'maps.apple.com')) {
- $icon = 'apple-logo';
+ $icon = jvbIcon('apple-logo');
}
if ($icon !== '') {
@@ -163,7 +187,7 @@
$this->getClassesAndStyles($block['attrs']??[]),
esc_url($url[1]),
esc_html($label[1]),
- jvbIcon($icon)
+ $icon
);
}
@@ -198,12 +222,18 @@
public function prerender_core_columns(array $block, ?string $content, ?WP_Block $parent):?string
{
- $tagName = array_key_exists('tagName', $block['attrs']) ? $block['attrs']['tagName'] : 'section';
+ jvbDump($block, 'columns');
+ $attrs = $block['attrs']??[];
+ $tagName = array_key_exists('tagName', $attrs) ? $attrs['tagName'] : 'section';
+ $classes = ['row', 'nowrap'];
+ if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true){
+ $classes[] = 'stack-small';
+ }
return sprintf(
'<%s%s>%s</%s>',
$tagName,
- $this->getClassesAndStyles($block['attrs']??[], ['row nowrap']),
+ $this->getClassesAndStyles($attrs, $classes),
$this->innerBlocks($block).'</section>',
$tagName
);
@@ -228,12 +258,36 @@
//core_home_link
//core_more
//core_nextpage
+ public function prerender_core_nextpage(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+
+ return str_replace('</a>', '</a></li>',str_replace('<a', '<li><a', wp_link_pages([
+ 'before' => '<nav class="pagination x-btw"><ul>',
+ 'after' => '</ul></nav>',
+ 'nextpagelink' => __('<span>Next </span>'.jvbIcon('caret-circle-right'), 'jvb'),
+ 'previouspagelink' => __(jvbIcon('caret-circle-left').'<span> Previous</span>', 'jvb'),
+ 'next_or_number'=> 'next',
+ 'echo' => false
+ ])));
+ }
public function prerender_core_separator(array $block, ?string $content, ?WP_Block $parent):?string
{
// jvbDump($block, 'separator');
// jvbDump($parent, 'Parent');
- return '<hr'.$this->getClassesAndStyles($block['attrs']??[]).'>';
+ $attrs = $block['attrs']??[];
+ $logo = '';
+ if (array_key_exists('className', $attrs) && $attrs['className'] === 'is-style-logo'){
+ $logo = apply_filters('jvbSeparatorLogo', 'logo');
+ if (!empty($logo)) {
+ $logo = jvbIcon($logo);
+ }
+ }
+ return sprintf(
+ '<hr%s>',
+ $this->getClassesAndStyles($attrs),
+// $logo
+ );
}
public function prerender_core_spacer(array $block, ?string $content, ?WP_Block $parent):?string
@@ -341,29 +395,60 @@
// jvbDump($block, 'media text');
// jvbDump($parent, 'Parent');
$ID = $this->imageID('', $block);
+ $attrs = $block['attrs']??[];
- $size = array_key_exists('mediaSizeSlug', $block['attrs']??[]) ? $block['attrs']['mediaSizeSlug'] : 'large';
+ $size = array_key_exists('mediaSizeSlug', $attrs) ? $attrs['mediaSizeSlug'] : 'large';
$imgLink = ($ID) ? $this->imageLink(true, $ID, 'tiny', $size) : '';
$inner = $this->innerBlocks($block);
- $classes = ['media-text', 'row'];
- if (array_key_exists('isStackedOnMobile', $block['attrs']??[])) {
- $classes[] = 'nowrap';
+ $classes = ['media-text', 'row', 'nowrap'];
+ if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true) {
+ $classes[] = 'stack-small';
}
- $content = '<div'.$this->getClassesAndStyles($block['attrs']??[], $classes).'>';
- $content .= (array_key_exists(
- 'mediaPosition',
- $block['attrs']??[]
- ) && $block['attrs']['mediaPosition'] == 'right') ?
- '<div>'.$inner.'</div><figure>'.$imgLink.'</figure>' :
- '<figure>'.$imgLink.'</figure><div>'.$inner.'</div>';
- $content .= '</div>';
- return $content;
+
+ $inside = array_key_exists('mediaPosition', $attrs) && $attrs['mediaPosition'] === 'right'
+ ? sprintf(
+ '<div>%s</div><figure>%s</figure>',
+ $inner, $imgLink
+ ) : sprintf(
+ '<figure>%s</figure><div>%s</div>',
+ $imgLink, $inner
+ );
+
+ return sprintf(
+ '<div%s>%s</div>',
+ $this->getClassesAndStyles($attrs, $classes),
+ $inside
+ );
}
//core_video
+ public function prerender_core_video(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ jvbDump($block, 'video');
+// jvbDump($parent, 'Parent');
+ $ID = $this->imageID('', $block);
+ if (!$ID) {
+ return '';
+ }
+
+ jvbDump($ID);
+
+ $title = (get_the_title($ID) !== '') ? '<b>'.get_the_title($ID).'</b>' : '';
+ $caption = (wp_get_attachment_caption($ID)) ?
+ '<figcaption>' .
+ $title .
+ wp_get_attachment_caption($ID) .
+ '</figcaption>' :
+ '<figcaption>' . $title . '</figcaption>';
+ $size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large';
+ return '<figure'.
+ $this->getClassesAndStyles($block['attrs']??[]).'>'.
+ $this->imageLink(true, $ID, 'tiny', $size) .
+ $caption.'</figure>';
+ }
/**
* Reusable blocks
@@ -379,10 +464,8 @@
//prerender_core_classic
public function prerender_core_heading(array $block, ?string $content, ?WP_Block $parent):?string
{
-// jvbDump($block, 'heading');
-// jvbDump($parent, 'Parent');
$level = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : '2';
- $content = $this->innerBlocks($block);
+ $content = $this->inside($block);
$id = sanitize_title(wp_strip_all_tags($this->stripTagContents('small', $content)));
return '<h'.$level.' id="'.$id.'"'.$this->getClassesAndStyles($block['attrs']??[]).'>'.
$content.
@@ -408,9 +491,12 @@
{
// jvbDump($block, 'paragraph');
// jvbDump($parent, 'Parent');
- return '<p'.$this->getClassesAndStyles($block['attrs']??[]).'>'.
- $this->innerBlocks($block).
- '</p>';
+ $inside = $this->inside($block);
+ return empty($inside) ? '' : sprintf(
+ '<p%s>%s</p>',
+ $this->getClassesAndStyles($block['attrs']??[]),
+ $inside
+ );
}
public function prerender_core_quote(array $block, ?string $content, ?WP_Block $parent): ?string
{
@@ -492,33 +578,50 @@
}
//core_pattern
- public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent):?string
+ public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent = null):?string
{
// jvbDump($block, 'site logo');
// jvbDump($parent, 'Parent');
+ $attrs = $block['attrs']??[];
$open = $close = '';
- if (!is_home() && !is_front_page()) {
- $open = '<a href="'.get_home_url().'" rel="home">';
+ if ((!is_home() && !is_front_page()) && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
+ $open = '<a href="'.get_home_url().'" rel="home" class="logo">';
$close = '</a>';
}
$img = get_theme_mod('custom_logo');
- $img = $this->image($img, 'tiny', 'thumbnail');
- $img = str_replace('<img', '<img'.$this->getClassesAndStyles($block['attrs']??[]), $img);
+ $img = sprintf(
+ '<figure%s>%s</figure>',
+ $this->getClassesAndStyles($attrs, ['logo']),
+ $this->image($img, 'tiny', 'thumbnail')
+ );
return $open.$img.$close;
}
- //core_site_title_tagline
+ public function prerender_core_site_tagline(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ $tagline = get_bloginfo('description');
+
+ return empty($tagline) ? '' : sprintf(
+ '<p%s>%s</p>',
+ $this->getClassesAndStyles($block['attrs']??[], ['tagline']),
+ $tagline
+ );
+ }
public function prerender_core_site_title(array $block, ?string $content, ?WP_Block $parent):?string
{
// jvbDump($block, 'site title');
// jvbDump($parent, 'Parent');
- $tag = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : 1;
+ $attrs = $block['attrs']??[];
+ $tag = (array_key_exists('level', $attrs)) ? $attrs['level'] : 1;
$tag = ($tag == 0) ? 'p' : 'h'.$tag;
$open = $close = '';
- if (!is_front_page()) {
- $open = '<a href="' . get_home_url() . '" rel="home">';
+ if (!is_front_page() && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
+ $open = sprintf(
+ '<a href="%s" rel="home">',
+ get_home_url()
+ );
$close = '</a>';
}
$class = ($tag === 'p') ?
@@ -526,11 +629,15 @@
$this->getClassesAndStyles($block['attrs']??[]);
- return '<'.$tag.$class.'>'.
- $open.
- get_bloginfo('name').
- $close.
- '</'.$tag.'>';
+ return sprintf(
+ '<%s%s>%s%s%s</%s>',
+ $tag,
+ $class,
+ $open,
+ get_bloginfo('name'),
+ $close,
+ $tag
+ );
}
/**
@@ -557,44 +664,70 @@
{
// jvbDump($block, 'navigation');
// jvbDump($parent, 'Parent');
+// jvbDump($block, 'navigation');
$ID = (array_key_exists('ref', $block['attrs']??[])) ? $block['attrs']['ref'] : false;
if (empty($block['innerBlocks']) && $ID && get_post($ID)) {
$block['innerBlocks'] = parse_blocks(get_post($ID)->post_content);
}
+ $attrs = $block['attrs']??[];
- $toggle = (array_key_exists('overlayMenu', $block['attrs']??[])
- && $block['attrs']['overlayMenu'] == 'never') ?
- '':
- '<button class="toggle main"
- data-action="toggle-menu"
- aria-label="Open Menu"
- aria-controls="navigation-' .$ID. '"
- aria-expanded="false">'.
- jvbIcon('list', ['title'=>'Toggle Menu']).
- jvbIcon('x', ['title'=>'Toggle Menu']).
- '</button>';
- $class = ($toggle === '') ?
- $this->getClassesAndStyles($block['attrs']??[], ['mobile']) :
- $this->getClassesAndStyles($block['attrs']??[]);
- $helpmenu = (get_the_title($ID) === 'Main') ?
- '<nav><ul>'.jvbNotificationMenu().jvbHelpMenu().'</ul></nav>' :
- '';
+ $toggle = '';
+ $classes = [];
+ if (!array_key_exists('overlayMenu', $attrs) || $attrs['overlayMenu'] !== 'never') {
+ $toggle = sprintf(
+ '<button class="toggle main"
+ data-action="toggle-menu"
+ aria-label="Open Menu"
+ aria-controls="navigation-%d"
+ aria-expanded="false">%s%s</button>',
+ $ID,
+ jvbIcon('list'),
+ jvbIcon('x')
+ );
+ $classes[] = 'mobile';
+ if (array_key_exists('overlayMenu', $attrs) && $attrs['overlayMenu'] === 'always') {
+ $classes[] = 'always';
+ }
+ }
+ if (!array_key_exists('layout', $attrs)) {
+ $classes[] = 'left';
+ $classes[] = 'row';
+ }
+ $class = $this->getClassesAndStyles($attrs, $classes);
+
+ $helpmenu = '';
+ $title = get_the_title($ID);
+ $isMain = false;
+ if ($title === 'Main') {
+ $isMain = true;
+ $helpmenu = sprintf(
+ '<nav><ul>%s%s</ul></nav>',
+ jvbNotificationMenu(),
+ jvbHelpMenu()
+ );
+ }
//Allows to add custom items to a menu, based on the menu name
- $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, get_the_title($ID));
- $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), get_the_title($ID), $block));
+ $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, $title, $ID);
+ $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), $title, $block));
- $main = str_starts_with($main, '<ul') ? $main : '<ul>'.$main.'</ul>';
+ $main = str_starts_with($main, '<ul') ? $main : sprintf('<ul>%s</ul>',$main);
- return '<nav'.$class.' id="navigation-' . $ID . '"aria-label="Navigation">
- <span class="screen-reader-text">
+ $skipToContent = $isMain ? '<span class="screen-reader-text">
<a href="#content">Skip to Content</a>
- </span>' .
- $toggle .
- $main.
- '</nav>'.$helpmenu;
+ </span>' : '';
+ return sprintf(
+ '<nav%s id="navigation-%d"aria-label="Navigation">
+ %s%s%s</nav>%s',
+ $class,
+ $ID,
+ $skipToContent,
+ $toggle,
+ $main,
+ $helpmenu
+ );
}
public function prerender_core_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
@@ -605,20 +738,20 @@
if (!array_key_exists('attrs', $block)) {
return '';
}
- $url = (str_starts_with($block['attrs']['url'],'/')) ?
- home_url($block['attrs']['url']) :
- $block['attrs']['url'];
+ $attrs = $block['attrs']??[];
+ $url = (str_starts_with($attrs['url'],'/')) ?
+ home_url($attrs['url']) :
+ $attrs['url'];
$current = (home_url($wp->request.'/') == $url);
- $temp = $block['attrs']??[];
- unset($temp['url']);
+ $attrs['url'] = $url;
$classes = ($current) ?
- $this->getClassesAndStyles($temp, ['current']):
- $this->getClassesAndStyles($temp);
+ $this->getClassesAndStyles($attrs, ['current']):
+ $this->getClassesAndStyles($attrs);
$aria = '';
if ($current) {
$aria = ' aria-current="page"';
}
- $linkOpen = $this->build_navigation_link($block['attrs']??[], $aria);
+ $linkOpen = $this->buildNavigationLink($attrs, $aria);
return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>';
@@ -634,22 +767,32 @@
$block['attrs']['url'];
$current = (home_url($wp->request) == $url);
- $temp = $block['attrs']??[];
- unset($temp['url']);
+ $attrs = $block['attrs']??[];
+ $attrs['url'] = $url;
$classes = ($current) ?
- $this->getClassesAndStyles($temp, ['has-submenu', 'current']):
- $this->getClassesAndStyles($temp, ['has-submenu']);
+ $this->getClassesAndStyles($attrs, ['has-submenu', 'current']):
+ $this->getClassesAndStyles($attrs, ['has-submenu']);
$aria = '';
if ($current) {
$aria = ' aria-current="page"';
}
$id = sanitize_title($block['attrs']['label']);
- $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
- $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].
- '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'.
- jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
- '</button><ul class="submenu" id='.$id.'-submenu">';
+ $linkOpen = $this->buildNavigationLink($attrs, $aria);
+ $content = sprintf(
+ '<li%s>%s%s</a>
+ <button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open %s Submenu" aria-expanded="false" aria-controls="%s-submenu">
+ %s
+ </button>
+ <ul class="submenu" id=%s-submenu">',
+ $classes,
+ $linkOpen,
+ $attrs['label'],
+ $attrs['label'],
+ $id,
+ jvbIcon('caret-down', ['title'=>'Toggle Submenu']),
+ $id
+ );
$content .= $this->innerBlocks($block);
$content .= '</ul></li>';
@@ -657,9 +800,8 @@
return $content;
}
- protected function build_navigation_link(array $attrs, string $aria):string
+ protected function buildNavigationLink(array $attrs, string $aria):string
{
- global $wp;
$url =(str_starts_with($attrs['url'],'/')) ?
home_url($attrs['url']) :
$attrs['url'];
@@ -694,8 +836,88 @@
* Theme Query Blocks
*/
//core_post_author
+
+ public function prerender_core_post_author(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ $attrs = $block['attrs'] ?? [];
+
+ $size = 96;
+ if (array_key_exists('avatarSize',$attrs) && is_int($attrs['avatarSize'])) {
+ $size = $attrs['avatarSize'];
+ }
+ $byline = $aOpen = $aClose = $avatar = $bio = '';
+ global $post;
+ $user = get_userdata($post->post_author);
+
+ if (!array_key_exists('showAvatar', $attrs) || $this->checkAttrs('showAvatar', $attrs)){
+ $avatar = get_avatar($post->post_author, $size);
+ }
+ if (!array_key_exists('showBio', $attrs) || $this->checkAttrs('showBio', $attrs)) {
+ $bio = wpautop($user->description);
+ }
+
+ $target = '';
+ if (array_key_exists('linkTarget', $attrs) && $attrs['linkTarget']=== '_blank') {
+ $target = ' target="_blank"';
+ }
+
+ if ($this->checkAttrs('isLink', $attrs)) {
+ $aOpen = sprintf(
+ '<a href="%s"%s>',
+ get_author_posts_url($post->post_author),
+ $target
+ );
+ $aClose = '</a>';
+ }
+
+ if (array_key_exists('byline', $attrs)) {
+ $byline = sprintf(
+ '<small>%s</small> — ',
+ $attrs['byline']
+ );
+ }
+
+ $name = $user->display_name;
+
+
+ return sprintf(
+ '<div%s>%s%s%s<p>%s%s%s%s</p>%s</div>',
+ $this->getClassesAndStyles($attrs, ['row','nowrap']),
+ $aOpen,
+ $avatar,
+ $aClose,
+ $aOpen,
+ $byline,
+ $name,
+ $aClose,
+ $bio
+ );
+ }
//core_post_author_biography
- //core_post_author_name
+
+ public function prerender_core_post_author_name(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ $attrs = $block['attrs']??[];
+ global $post;
+ $aOpen = $aClose = '';
+ if ($this->checkAttrs('isLink', $attrs)) {
+
+ $aOpen = sprintf(
+ '<a href="%s" rel="author">',
+ get_author_posts_url($post->post_author)
+ );
+ $aClose = '</a>';
+ }
+
+ $author = get_userdata($post->post_author);
+ return sprintf(
+ '<p%s>%s%s%s</p>',
+ $this->getClassesAndStyles($attrs, ['author']),
+ $aOpen,
+ $author->display_name,
+ $aClose
+ );
+ }
public function prerender_core_post_content(array $block, ?string $content, ?WP_Block $parent):?string
{
// jvbDump($block, 'post content');
@@ -707,9 +929,21 @@
if ($content == '') {
if(is_singular()) {
- global $post;
+ global $post, $page;
- $block['innerBlocks'] = parse_blocks($post->post_content);
+ $pages = explode('<!--nextpage-->', $post->post_content);
+ $currentContent = $pages[max(0, $page - 1)] ?? $pages[0];
+
+
+ if ($page > 1 && !str_contains($currentContent, '<!--nextpage-->')) {
+ $currentContent = str_replace('<!-- /wp:nextpage -->','', $currentContent);
+ $currentContent .= '
+ <!-- wp:nextpage -->
+ <!--nextpage-->
+ <!-- /wp:nextpage -->';
+ }
+
+ $block['innerBlocks'] = parse_blocks($currentContent);
$result = $this->innerBlocks($block);
}else {
$result = '';
@@ -723,31 +957,118 @@
//core_post_date
public function prerender_core_post_date(array $block, ?string $content, ?WP_Block $parent):?string
{
+
// jvbDump($block, 'post date');
+// return null;
+ $attrs = $block['attrs']??[];
+ $postDate = null;
+ $itemProp = 'datePublished';
+ $format = array_key_exists('format', $attrs) ? $attrs['format'] : 'M d, Y';
+ $dateFormat = null;
+ if (array_key_exists('displayType', $attrs)) {
+ switch ($attrs['displayType']) {
+ case 'displayType':
+ $postDate = get_post_modified_time('c');
+ $dateFormat = get_post_modified_time($format);
+ $itemProp = 'dateModified';
+ break;
+
+
+ }
+ }
+ $postDate = is_null($postDate) ? get_the_date('c') : $postDate;
+ $dateFormat = is_null($dateFormat) ? get_the_date($format) : $dateFormat;
+
+
+
+ $aOpen = $aClose = '';
+ if ($this->checkAttrs('isLink', $attrs) && !is_singular()) {
+ $aOpen = sprintf(
+ '<a href="%s">',
+ get_the_permalink()
+ );
+ $aClose = '</a>';
+ }
// jvbDump($parent, 'Parent');
- $postDate = get_the_date('c');
- return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']??[]).'>'.get_the_date().'</time>';
+
+ return sprintf(
+ '<time datetime="%s" itemprop="%s"%s>%s%s%s</time>',
+ $postDate,
+ $itemProp,
+ $this->getClassesAndStyles($attrs),
+ $aOpen,
+ $dateFormat,
+ $aClose
+ );
}
//core_post_excerpt
public function prerender_core_post_excerpt(array $block, ?string $content, ?WP_Block $parent):?string
{
- return wpautop(get_the_excerpt());
+ $attrs = $block['attrs']??[];
+
+ $moreText = array_key_exists('moreText', $attrs) ? $attrs['moreText'] : 'Read more '.jvbIcon('arrow-circle-right');
+ $showMoreOnNewLine = !array_key_exists('showMoreOnNewLine', $attrs) || $this->checkAttrs('showMoreOnNewLine', $attrs);
+// jvbDump($block);
+// jvbDump($showMoreOnNewLine);
+
+ $excerpt = array_filter(explode('<p>',wpautop(get_the_excerpt())));
+ $classes = $this->getClassesAndStyles($attrs);
+ $excerpt = array_map(function ($line) use ($classes) {
+ return sprintf(
+ '<p%s>%s',
+ $classes,
+ $line
+ );
+ }, $excerpt);
+
+ if (!empty($moreText)) {
+ if ($showMoreOnNewLine) {
+ $excerpt[] = sprintf(
+ '<p%s><a href="%s" class="read-more">%s</a></p>',
+ $classes,
+ get_the_permalink(),
+ $moreText
+ );
+ } else {
+ $last = array_key_last($excerpt);
+ $excerpt[$last] = str_replace('</p>', sprintf('<a href="%s" class="read-more">%s</a>',
+ get_the_permalink(),
+ $moreText), $excerpt[$last]);
+ }
+ }
+ return implode('',$excerpt);
}
public function prerender_core_post_featured_image(array $block, ?string $content, ?WP_Block $parent):?string
{
// jvbDump($block, 'featured image');
// jvbDump($parent, 'Parent');
global $post;
+ $attrs = $block['attrs']??[];
$ID = get_post_thumbnail_id($post->ID);
- $aOpen = $aClose = '';
- if(!is_single($post->ID)) {
+ $aspectRatio = $aOpen = $aClose = '';
+ if(!is_single($post->ID) && $this->checkAttrs('isLink', $attrs)) {
$aOpen = '<a href="'.get_the_permalink($post->ID).'">';
$aClose = '</a>';
}
+ if (array_key_exists('aspectRatio', $attrs)) {
+ $aspectRatio = $attrs['aspectRatio'];
+ }
- return '<figure'.$this->getClassesAndStyles($block['attrs']??[]).'>'.$aOpen.
- apply_filters('jvbCoreFeaturedImage', $this->image($ID), $post->post_type).
- $aClose.'</figure>';
+ $img = apply_filters('jvbCoreFeaturedImage', '', $post->post_type, $attrs);
+
+ if (empty($img)) {
+ $img = $this->image($ID);
+ $img = empty($aspectRatio) ? $img : str_replace('<img', '<img style="aspect-ratio:'.$aspectRatio.';"', $img);
+ }
+
+
+ return !empty($img) ? sprintf(
+ '<figure%s>%s%s%s</figure>',
+ $this->getClassesAndStyles($attrs),
+ $aOpen,
+ $img,
+ $aClose,
+ ):'';
}
//core_post_navigation_link
public function prerender_core_post_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
@@ -801,23 +1122,27 @@
);
}
//core_post_template
- public function render_core_post_template(array $block, string $content):string
+ public function prerender_core_post_template(array $block, ?string $content):?string
{
- global $wp_query;
-
$inner = '';
- $block['innerBlocks'][0]['attrs']['tagName'] = 'li';
- if ($wp_query->have_posts()) {
- while ($wp_query->have_posts()) {
- $wp_query->the_post();
- $inner .= $this->innerBlocks($block);
- }
- wp_reset_postdata();
+ if (!static::$currentLoop) {
+ jvbDump('No loop stored');
+ return $content;
}
+ if (static::$currentLoop->have_posts()) {
+ while (static::$currentLoop->have_posts()) {
+ static::$currentLoop->the_post();
+ $inner .= sprintf(
+ '<li>%s</li>',
+ $this->innerBlocks($block, '','',$block)
+ );
+ }
+ }
return sprintf(
- '<ul class="loop">%s</ul>',
+ '<ul%s>%s</ul>',
+ $this->getClassesAndStyles($block['attrs']??[], ['loop']),
$inner
);
}
@@ -828,16 +1153,17 @@
return '';
}
$terms = get_the_terms(get_the_ID(), $block['attrs']['term']);
+ $attrs = $block['attrs']??[];
$out = '';
if ($terms && !is_wp_error($terms)) {
$out = sprintf(
'<ul%s>',
- $this->getClassesAndStyles($block['attrs'], ['term-list', 'row', 'start'])
+ $this->getClassesAndStyles($attrs, ['term-list', 'row', 'left'])
);
- if (array_key_exists('prefix', $block['attrs']??[])) {
+ if (array_key_exists('prefix', $attrs)) {
$out .= sprintf(
'<li class="prefix">%s</li>',
- $block['attrs']['prefix']
+ $attrs['prefix']
);
}
foreach($terms as $term) {
@@ -847,10 +1173,10 @@
html_entity_decode($term->name)
);
}
- if (array_key_exists('suffix', $block['attrs'])) {
+ if (array_key_exists('suffix', $attrs)) {
$out .= sprintf(
'<li class="suffix">%s</li>',
- $block['attrs']['suffix']
+ $attrs['suffix']
);
}
$out .= '</ul>';
@@ -860,145 +1186,388 @@
//core_post_time_to_read
public function prerender_core_post_title(array $block, ?string $content, ?WP_Block $parent):?string
{
-// jvbDump($block, 'post content');
// jvbDump($parent, 'Parent');
$open = $close = '';
- if (array_key_exists('isLink', $block['attrs']??[])) {
- $rel = (array_key_exists('rel', $block['attrs']??[])) ?
+ $attrs = $block['attrs']??[];
+ if ($this->checkAttrs('isLink', $attrs)) {
+ $rel = (array_key_exists('rel', $attrs)) ?
' rel="'.$block['attrs']['rel'].'"' :
'';
- $target = (array_key_exists('linkTarget', $block['attrs']??[])) ?
+ $target = (array_key_exists('linkTarget', $attrs)) ?
' target="'.$block['attrs']['linkTarget'].'"' :
'';
$open = '<a href="' . get_the_permalink() . '"' . $rel . $target . '>';
$close = '</a>';
}
- if (is_singular(BASE.'partner')) {
- $open .= '<small>edmonton.ink partner:</small> ';
- }
- $level = (array_key_exists('attrs', $block) &&
- array_key_exists('level', $block['attrs'])) ?
- $block['attrs']['level'] :
- 2;
- return '<h'.$level.$this->getClassesAndStyles($block['attrs']??[]).'>'.
- $open.get_the_title().$close.
- '</h'.$level.'>';
+
+ $level = $attrs['level']??2;
+
+ $title = (!static::$currentLoop && !is_singular())
+ ? get_the_title(get_queried_object_id())
+ : get_the_title();
+
+ return sprintf(
+ '<h%s%s>%s%s%s</h%s>',
+ $level,
+ $this->getClassesAndStyles($attrs),
+ $open,
+ $title,
+ $close,
+ $level
+ );
}
-
- public function render_core_query(array $block, string $content): string
+ public function prerender_core_query(array $block, ?string $content):?string
{
+ global $wp_query;
+ $inherit = $block['attrs']['inherit'] ?? false;
-// $queryID = $block['attrs']['queryId'] ?? null;
-// $inherit = $block['attrs']['inherit'] ?? false;
-//
-// if ($inherit) {
-// global $wp_query;
-// $loop = $wp_query;
-// } else {
-// $args = [];
-// foreach (($block['attrs']['query'] ?? []) as $key => $value) {
-// if (empty($value)) {
-// continue;
-// }
-// switch ($key) {
-// case 'postType':
-// if ($value === BASE.'progress'){
-// $args['post_parent'] = 0;
-// }
-// $args['post_type'] = $value;
-// break;
-// case 'perPage':
-// $args['posts_per_page'] = $value;
-// break;
-// case 'orderBy':
-// $args['orderby'] = $value;
-// break;
-// case 'taxQuery':
-// $taxQuery = [];
-// foreach ($value as $tax => $terms) {
-// $taxQuery[] = [
-// 'taxonomy' => $tax,
-// 'terms' => $terms
-// ];
-// }
-// if (!empty($taxQuery)) {
-// $args['tax_query'] = $taxQuery;
-// if (count($taxQuery) > 1) {
-// $args['tax_query']['relation'] = 'OR';
-// }
-// }
-// break;
-// case 'sticky':
-// if ($value === 'ignore') {
-// $args['ignore_sticky_posts'] = true;
-// } else if ($value === 'exclude'){
-// $args['post__not_in'] = get_option('sticky_posts');
-// } else if ($value === 'only') {
-// $args['include'] = get_option('sticky_posts');
-// }
-// break;
-// case 'search':
-// $args['s'] = $value;
-// break;
-// default:
-// $args[$key] = $value;
-// break;
-//
-// }
-// }
-// $search = 'query-' . $queryID;
-// foreach ($_GET as $key => $value) {
-// if (str_contains($key, $search)) {
-// $key = str_replace($search, '', $key);
-// if ($key === 'page') {
-// $args['paged'] = (int)$value;
-// }
-// }
-// }
-// $loop = new WP_Query($args);
-// }
-//
-// $inner = '';
-// foreach ($block['innerBlocks'] as $innerBlock) {
-// switch ($innerBlock['blockName']) {
-// case 'core/post-template':
-// $inner .= '<section class="item-grid">';
-// if ($loop->have_posts()) {
-// while ($loop->have_posts()) {
-// $loop->the_post();
-// $postType = get_post_type();
-// $inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>';
-// }
-// }
-// $inner .= '</section>';
-// break;
-// }
-// }
-//
-// // Reset only after a custom query, not the main query
-// if (!$inherit) {
-// wp_reset_postdata();
-// }
+ if ($inherit) {
+ static::$currentLoop = $wp_query;
+ } else {
+ static::$currentLoop = new WP_Query($this->buildQueryArgs($block['attrs']));
+ }
+ static::$currentQueryId = $block['attrs']['queryId'] ?? null;
- $tagName = $block['attrs']['tagName'] ?? 'div';
-// return sprintf(
-// '<%s class="loop">%s</%s>',
-// $tagName,
-// $this->innerBlocks($block),
-// $tagName
-// );
- return $this->innerBlocks($block);
+ static::$originalQuery = $wp_query;
+
+ $inside = $this->innerBlocks($block);
+
+ if (str_contains($inside, 'loop')) {
+ $classes = $this->getClassesAndStyles($block['attrs'] ?? [], ['loop']);
+ $classes = str_replace(' class="', '', $classes);
+ $classes = strtok($classes, '"');
+ $inside = str_replace('loop', $classes, $inside);
+ }
+
+
+ static::$currentQueryId = null;
+ static::$currentLoop = null;
+
+ wp_reset_postdata();
+ return $inside;
}
+// public function render_core_query(array $block, string $content): string
+// {
+// $inside = $this->innerBlocks($block);
+// if (str_contains($inside, 'loop')) {
+// $classes = $this->getClassesAndStyles($block['attrs']??[], ['loop']);
+// $classes = str_replace(' class="', '', $classes);
+// $classes = strtok($classes, '"');
+//
+// $inside = str_replace('loop', $classes, $inside);
+// }
+// return $inside;
+// }
+ protected function buildQueryArgs(array $attrs): array
+ {
+ $queryID = $attrs['queryId'] ?? null;
+ $args = [];
+ foreach (($attrs['query'] ?? []) as $key => $value) {
+ if (empty($value)) continue;
+ switch ($key) {
+ case 'postType': $args['post_type'] = $value; break;
+ case 'perPage': $args['posts_per_page'] = $value; break;
+ case 'orderBy': $args['orderby'] = $value; break;
+ case 'sticky':
+ match ($value) {
+ 'ignore' => $args['ignore_sticky_posts'] = true,
+ 'exclude' => $args['post__not_in'] = get_option('sticky_posts'),
+ 'only' => $args['post__in'] = get_option('sticky_posts'),
+ default => null
+ };
+ break;
+ case 'taxQuery':
+ $taxQuery = array_map(fn($tax, $terms) => [
+ 'taxonomy' => $tax, 'terms' => $terms
+ ], array_keys($value), $value);
+ if (count($taxQuery) > 1) $taxQuery['relation'] = 'OR';
+ $args['tax_query'] = $taxQuery;
+ break;
+ case 'search': $args['s'] = $value; break;
+ default: $args[$key] = $value; break;
+ }
+ }
+
+ // Handle pagination from query string
+ $search = 'q-' . $queryID.'-';
+ foreach ($_GET as $key => $value) {
+ if (str_contains($key, $search) && str_replace($search, '', $key) === 'page') {
+ $args['paged'] = (int)$value;
+ }
+ }
+ return $args;
+ }
+
+ protected function buildPaginationUrl(int $page): string
+ {
+ $param = 'q-' . static::$currentQueryId . '-page';
+ $url = remove_query_arg($param);
+ return $page > 1 ? add_query_arg($param, $page, $url) : $url;
+ }
+
+ protected function getCurrentPage(): int
+ {
+ $param = 'q-' . static::$currentQueryId . '-page';
+ return isset($_GET[$param]) ? (int)$_GET[$param] : 1;
+ }
+// public function render_core_query(array $block, string $content): string
+// {
+//
+//// $queryID = $block['attrs']['queryId'] ?? null;
+//// $inherit = $block['attrs']['inherit'] ?? false;
+////
+//// if ($inherit) {
+//// global $wp_query;
+//// $loop = $wp_query;
+//// } else {
+//// $args = [];
+//// foreach (($block['attrs']['query'] ?? []) as $key => $value) {
+//// if (empty($value)) {
+//// continue;
+//// }
+//// switch ($key) {
+//// case 'postType':
+//// if ($value === BASE.'progress'){
+//// $args['post_parent'] = 0;
+//// }
+//// $args['post_type'] = $value;
+//// break;
+//// case 'perPage':
+//// $args['posts_per_page'] = $value;
+//// break;
+//// case 'orderBy':
+//// $args['orderby'] = $value;
+//// break;
+//// case 'taxQuery':
+//// $taxQuery = [];
+//// foreach ($value as $tax => $terms) {
+//// $taxQuery[] = [
+//// 'taxonomy' => $tax,
+//// 'terms' => $terms
+//// ];
+//// }
+//// if (!empty($taxQuery)) {
+//// $args['tax_query'] = $taxQuery;
+//// if (count($taxQuery) > 1) {
+//// $args['tax_query']['relation'] = 'OR';
+//// }
+//// }
+//// break;
+//// case 'sticky':
+//// if ($value === 'ignore') {
+//// $args['ignore_sticky_posts'] = true;
+//// } else if ($value === 'exclude'){
+//// $args['post__not_in'] = get_option('sticky_posts');
+//// } else if ($value === 'only') {
+//// $args['include'] = get_option('sticky_posts');
+//// }
+//// break;
+//// case 'search':
+//// $args['s'] = $value;
+//// break;
+//// default:
+//// $args[$key] = $value;
+//// break;
+////
+//// }
+//// }
+//// $search = 'query-' . $queryID;
+//// foreach ($_GET as $key => $value) {
+//// if (str_contains($key, $search)) {
+//// $key = str_replace($search, '', $key);
+//// if ($key === 'page') {
+//// $args['paged'] = (int)$value;
+//// }
+//// }
+//// }
+//// $loop = new WP_Query($args);
+//// }
+////
+//// $inner = '';
+//// foreach ($block['innerBlocks'] as $innerBlock) {
+//// switch ($innerBlock['blockName']) {
+//// case 'core/post-template':
+//// $inner .= '<section class="item-grid">';
+//// if ($loop->have_posts()) {
+//// while ($loop->have_posts()) {
+//// $loop->the_post();
+//// $postType = get_post_type();
+//// $inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>';
+//// }
+//// }
+//// $inner .= '</section>';
+//// break;
+//// }
+//// }
+////
+//// // Reset only after a custom query, not the main query
+//// if (!$inherit) {
+//// wp_reset_postdata();
+//// }
+//
+// $tagName = $block['attrs']['tagName'] ?? 'div';
+//// return sprintf(
+//// '<%s class="loop">%s</%s>',
+//// $tagName,
+//// $this->innerBlocks($block),
+//// $tagName
+//// );
+// return $this->innerBlocks($block);
+// }
//core_query_no_results
+ public function prerender_core_query_no_results(array $block, ?string $content):?string
+ {
+ if (!static::$currentLoop || static::$currentLoop->have_posts()) {
+ return '';
+ }
+
+ $inside = $this->innerBlocks($block);
+ return empty($inside) ? '' : sprintf(
+ '<div%s>%s</div>',
+ $this->getClassesAndStyles($block['attrs']??[], ['no-results']),
+ $inside
+ );
+ }
//core_query_pagination
+ public function prerender_core_query_pagination(array $block, ?string $content):?string
+ {
+ return sprintf(
+ '<nav%s>%s</nav>',
+ $this->getClassesAndStyles($block['attrs']??[], ['pagination', 'condensed','btw']),
+ $this->innerBlocks($block)
+ );
+ }
+
//core_query_pagination_next
+
+ public function prerender_core_query_pagination_next(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ if (!static::$currentLoop) return '';
+
+ $currentPage = $this->getCurrentPage();
+ $maxPages = static::$currentLoop->max_num_pages;
+
+ if ($currentPage >= $maxPages) return '';
+
+ $nextLabel = $rArrow = '';
+ $type = get_post_type_object(get_post_type())->label;
+ if ($parent) {
+ $attrs = $parent->attributes;
+ if (array_key_exists('paginationArrow', $attrs)){
+ $rArrow = match($attrs['paginationArrow']) {
+ 'chevron' => jvbIcon('caret-circle-right'),
+ default => jvbIcon('arrow-circle-right')
+ };
+ }
+ if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
+
+ $nextLabel = 'Next '.$type;
+ }
+ } else {
+ $rArrow = jvbIcon('caret-circle-right');
+ }
+
+ $aOpen = sprintf(
+ '<a class="nav next" href="%s" title="Next %s">',
+ $this->buildPaginationUrl($currentPage + 1),
+ $type
+ );
+ $aClose = '</a>';
+ return sprintf(
+ '%s%s%s%s',
+ $aOpen,
+ $nextLabel,
+ $rArrow,
+ $aClose
+ );
+ }
//core_query_pagination_numbers
+ public function prerender_core_query_pagination_numbers(array $block, ?string $content):?string
+ {
+ if (!static::$currentLoop) return '';
+ $currentPage = $this->getCurrentPage();
+ $maxPages = (int)static::$currentLoop->max_num_pages;
+
+ $attrs = $block['attrs']??[];
+ if ($maxPages <= 1) return '';
+
+ $midSize = $attrs['midSize'] ?? 2;
+ $endSize = 1;
+
+
+ $items = '';
+ $gap = false;
+ for ($i = 1; $i <= $maxPages; $i++) {
+ if (($i <= min($endSize + 1, $maxPages)) ||
+ ($i >= max(1, $currentPage - $midSize) && $i <= min($maxPages, $currentPage + $midSize)) ||
+ ($i >= max(1, $maxPages - $endSize) && $i <= $maxPages)) {
+ $gap = true;
+ $items .= ($i === $currentPage)
+ ? sprintf('<li aria-current="page" class="current">%d</li>', $i)
+ : sprintf('<li><a href="%s">%d</a></li>', $this->buildPaginationUrl($i), $i);
+ } elseif ($gap) {
+ $gap = false;
+ $items .= sprintf(
+ '<li class="dots"><span>%s</span></li>',
+ jvbIcon('dots-three')
+ );
+ }
+ }
+
+ return sprintf('<ul%s>%s</ul>',
+ $this->getClassesAndStyles($attrs, ['row', 'nowrap']),
+ $items
+ );
+ }
//core_query_pagination_previous
+ public function prerender_core_query_pagination_previous(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+
+ if (!static::$currentLoop) return '';
+
+ $currentPage = $this->getCurrentPage();
+ $maxPages = static::$currentLoop->max_num_pages;
+
+ if ($currentPage <= 1) return '';
+
+ $nextLabel = $rArrow = '';
+ $type = get_post_type_object(get_post_type())->label;
+ if ($parent) {
+ $attrs = $parent->attributes;
+ if (array_key_exists('paginationArrow', $attrs)){
+ $rArrow = match($attrs['paginationArrow']) {
+ 'chevron' => jvbIcon('caret-circle-left'),
+ default => jvbIcon('arrow-circle-left')
+ };
+ }
+ if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
+
+ $nextLabel = 'Previous '.$type;
+ }
+ } else {
+ $rArrow = jvbIcon('caret-circle-left');
+ }
+
+ $aOpen = sprintf(
+ '<a class="nav prev" href="%s" title="Previous %s">',
+ $this->buildPaginationUrl($currentPage - 1),
+ $type
+ );
+ $aClose = '</a>';
+ return sprintf(
+ '%s%s%s%s',
+ $aOpen,
+ $nextLabel,
+ $rArrow,
+ $aClose
+ );
+ }
+
public function prerender_core_query_title(array $block, ?string $content, ?WP_Block $parent):?string
{
- jvbDump($block);
+ jvbDump($block, 'query title');
$attr = $block['attrs'];
$name = '';
$showPrefix = $attr['showPrefix']??false;
@@ -1050,7 +1619,7 @@
$before = apply_filters('jvbAboveHeader', '');
if (!empty($before)) {
$before = sprintf(
- '<aside class="pre header row btw">%s</aside>',
+ '<aside class="pre header row x-btw">%s</aside>',
$before
);
}
@@ -1059,7 +1628,7 @@
$after = apply_filters('jvbBelowHeader', $after);
if (!empty($after)) {
$after = sprintf(
- '<aside class="sub header row btw">%s</aside>',
+ '<aside class="sub header row x-btw">%s</aside>',
$after
);
}
@@ -1114,14 +1683,14 @@
//
// $beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader);
// if ($beforeHeader !== '') {
-// $beforeHeader = '<aside class="pre header row btw">'.$beforeHeader.'</aside>';
+// $beforeHeader = '<aside class="pre header row x-btw">'.$beforeHeader.'</aside>';
// }
// $themeSwitch = jvbDarkModeToggle();
// $breadcrumbs = BreadcrumbManager::getInstance()->renderNavigation();
// $afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
//
// if ($afterHeader !== '') {
-// $afterHeader = '<aside class="sub header row btw">'.$afterHeader.'</aside>';
+// $afterHeader = '<aside class="sub header row x-btw">'.$afterHeader.'</aside>';
// }
// $footerText = '<div class="scroll-progress"><div class="bar"></div>
//</div>';
@@ -1149,21 +1718,325 @@
* Widgets Blocks
*/
//core_archives
+ public function render_core_archives(array $block, string $content):string
+ {
+ jvbDump($block, 'archives');
+ $attrs = $block['attrs']??[];
+ $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
+
+ $replace = strtok($content,'>').'>';
+ $content = str_replace($replace, '', $content);
+
+ if ($isDropdown) {
+ $content = sprintf(
+ '<div%s>%s',
+ $this->getClassesAndStyles($attrs, ['archive dropdown']),
+ $content
+ );
+ } else {
+ $content = sprintf(
+ '<ul%s>%s',
+ $this->getClassesAndStyles($attrs, ['archive-list']),
+ $content
+ );
+ }
+
+ return $content;
+ }
//core_calendar
+ public function render_core_calendar(array $block, string $content):string
+ {
+ $content = $this->inside($block, false, $content);
+ $replace = strtok($content, '>').'>';
+ $content = str_replace($replace, '', $content);
+ return sprintf(
+ '<table%s>%s',
+ $this->getClassesAndStyles($block['attrs']??[], ['calendar']),
+ $content
+ );
+ }
//core_categories
+ public function prerender_core_categories(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+ $attrs = $block['attrs']??[];
+ $args = [
+ 'taxonomy' => 'category',
+ 'hide_empty' => !$this->checkAttrs('showEmpty', $attrs)
+ ];
+
+ $showHierarchy = $this->checkAttrs('showHierarchy', $attrs);
+ if ($this->checkAttrs('showOnlyTopLevel', $attrs) || $showHierarchy){
+ $args['parent'] = 0;
+ }
+
+ $terms = $this->getTerms($args, $showHierarchy);
+ if (!$terms){
+ return '';
+ }
+
+
+ $showPostCounts = $this->checkAttrs('showPostCounts', $attrs);
+ $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
+
+ if ($isDropdown) {
+ $this->counter('core_categories');
+ }
+
+ $tax = get_taxonomy($args['taxonomy']);
+ $taxonomyName = $tax->label??'Categories';
+ $taxonomySingular = $tax->labels->singular_name??'Category';
+ $inner = $this->buildTermList($terms, $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts);
+ if ($isDropdown) {
+ return sprintf(
+ '<div%s>%s</div>',
+ $this->getClassesAndStyles($attrs, ['taxonomy-dropdown']),
+ $inner
+ );
+ }
+ return sprintf(
+ '<ul%s>%s</ul>',
+ $this->getClassesAndStyles($attrs, ['taxonomy-list', jvbNoBase($args['taxonomy'])]),
+ $inner
+ );
+ }
+ public function getTerms(array $args, bool $showHierarchy = false):array|false
+ {
+ $terms = get_terms($args);
+ if (!$terms || is_wp_error($terms)) {
+ return false;
+ }
+ $terms = array_map(function ($term) {
+ return (array) $term;
+ }, $terms);
+
+ if ($showHierarchy) {
+ $terms = array_map(function ($term) use ($args) {
+ $args['parent'] = $term['term_id'];
+ $children = $this->getTerms($args, true);
+ $term['children'] = $children?:[];
+ return $term;
+ }, $terms);
+ }
+
+ return $terms;
+ }
+ protected function buildTermList(array $terms, string $taxonomyName, string $taxonomySingular, bool $isDropdown, bool $showPostCounts, bool $isOpening = true, int $level = 0):string
+ {
+ $out = '';
+ if ($isOpening) {
+ $out = $isDropdown ?
+ sprintf(
+ '<label for="taxonomy-select-%s">%s</label>
+ <select name="%s_name" id="taxonomy-select-%s"><option value="">Select %s</option>',
+ static::$counters['core_categories'],
+ $taxonomyName,
+ str_replace('-', '_',sanitize_title(strtolower($taxonomyName))),
+ static::$counters['core_categories'],
+ $taxonomyName
+ ) :
+ '';
+ } elseif (!$isDropdown) {
+ $out .= '<ul>';
+ }
+
+
+ $prefix = '';
+ if ($isDropdown) {
+ $base = ' ';
+ for ($i = 1; $i <= $level; $i++) {
+ $prefix .= $base;
+ }
+ $prefix .= empty($prefix) ? '' : '- ';
+ }
+
+ $theTerms = array_map(function ($term) use ($taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, $prefix, $level) {
+ if ($isDropdown) {
+ return sprintf(
+ '<option value="%s">%s%s%s</option>%s',
+ $term['slug'],
+ $prefix,
+ $term['name'],
+ $showPostCounts ? ' ('.$term['count'].')' : '',
+ empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
+ );
+ }
+ return sprintf(
+ '<li><a href="%s">%s%s</a>%s</li>',
+ get_term_link($term['term_id']),
+ $term['name'],
+ $showPostCounts ? ' <span class="count">'.$term['count'].'</span>' : '',
+ empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
+ );
+ }, $terms);
+
+ $out .= implode('', $theTerms);
+
+ if ($isOpening) {
+ $out .= $isDropdown ?
+ '</select>' :
+ '';
+ } else if (!$isDropdown) {
+ $out .= '</ul>';
+ }
+
+
+ return $out;
+ }
//core_html
//core_latest_comments
//core_latest_posts
+ public function prerender_core_latest_posts(array $block, ?string $content, ?WP_Block $parent):?string {
+ $attrs = $block['attrs']??[];
+// jvbDump($block, 'latest posts');
+
+ $args = [];
+ $title = 'Latest Posts';
+ $args['order'] = array_key_exists('order', $attrs) ? strtoupper($attrs['order']) : 'DESC';
+ $args['orderby'] = array_key_exists('orderBy', $attrs) ? $attrs['orderBy'] : 'date';
+ $args['posts_per_page'] = array_key_exists('postsToShow', $attrs) ? $attrs['postsToShow'] : 5;
+
+ if (array_key_exists('categories', $attrs)) {
+ $list = jvbCommaList(array_column($attrs['categories'], 'name'));
+ $args['tax_query'] = [];
+ $args['tax_query'][] = [
+ 'taxonomy' => 'category',
+ 'terms' => array_column($attrs['categories'], 'id')
+ ];
+ $title .= ' in '.$list;
+ }
+
+ $posts = new WP_Query($args);
+
+ if (!$posts->have_posts()) {
+ return '';
+ }
+ $posts = array_map(function ($post) use ($attrs) {
+ $img = $this->checkAttrs('displayFeaturedImage', $attrs)
+ ? $this->image(get_post_thumbnail_id($post->ID), 'tiny', 'thumbnail')
+ : '';
+
+ $author = $this->checkAttrs('displayAuthor', $attrs)
+ ? sprintf(
+ '<a href="%s">%s</a>',
+ get_author_posts_url($post->post_author),
+ get_userdata($post->post_author)->display_name
+ )
+ : '';
+
+ $date = $this->checkAttrs('displayPostDate', $attrs)
+ ? sprintf(
+ '<time datetime="%s">%s</time>',
+ date('Y-m-d', strtotime($post->post_date)),
+ date_i18n('M j, Y', strtotime($post->post_date))
+ )
+ : '';
+ $authorDate = $author;
+ if (!empty($authorDate) && !empty($date)) {
+ $authorDate .= ' | '.$date;
+ } else if (!empty($date)) {
+ $authorDate = $date;
+ }
+
+ $excerpt = '';
+ if ($this->checkAttrs('displayPostContent', $attrs)) {
+ if (array_key_exists('excerptLength', $attrs)) {
+ $excerpt = wp_trim_words(get_the_content($post->ID), $attrs['excerptLength'], '...');
+ } else {
+ $excerpt = get_the_excerpt($post->ID);
+ }
+ }
+ if (!empty($excerpt)) {
+ $excerpt = wpautop($excerpt);
+ }
+
+ return sprintf(
+ '<li>%s<p><a href="%s">%s</a>%s</p>%s</li>',
+ $img,
+ get_the_permalink($post->ID),
+ $post->post_title,
+ !empty($authorDate) ? ' <small>— '.$authorDate.'</small>' : '',
+ $excerpt
+ );
+ }, $posts->posts);
+
+ wp_reset_postdata();
+ return sprintf(
+ '<ul%s>%s</ul>',
+// $title,
+ $this->getClassesAndStyles($attrs, ['post-list']),
+ implode('', $posts)
+ );
+ }
//core_page_list
- //core_page_list_item
- //core_rss
+ public function prerender_core_page_list(array $block, ?string $content, ?WP_Block $parent):?string{
+ $attrs = $block['attrs']??[];
+ $parent = array_key_exists('parentPageID', $attrs) ? $attrs['parentPageID'] : 0;
+ $pages = new WP_Query([
+ 'post_type' => 'page',
+ 'posts_per_page' => -1,
+ 'parent' => $parent
+ ]);
+
+ if (!$pages->have_posts()) {
+ return '';
+ }
+ $inside = [];
+ foreach($pages->posts as $page) {
+ jvbDump($page);
+ $inside[] = sprintf(
+ '<li><a href="%s">%s</a>',
+ get_the_permalink($page->ID),
+ $page->post_title
+ );
+ }
+ wp_reset_postdata();
+ return sprintf(
+ '<ul%s>%s</ul>',
+ $this->getClassesAndStyles($attrs, ['page-list']),
+ implode('',$inside)
+ );
+ }
+ //core_page_list_item (doesn't seem to be a thing)
+// public function prerender_core_page_list_item(array $block, ?string $content, ?WP_Block $parent):?string{
+// return $content;
+// }
+ //core_
+// public function prerender_core_rss(array $block, ?string $content, ?WP_Block $parent):?string
+// {
+// jvbDump($block, 'rss');
+// return $content;
+// }
//core_search
+ public function prerender_core_search(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+// jvbDump($block, 'search');
+ $attrs = $block['attrs']??[];
+ $label = array_key_exists('label', $attrs) && !empty($attrs['label']) ? $attrs['label'] : '';
+ if (array_key_exists('showLabel', $attrs) && $attrs['showLabel'] === false) {
+ $label = '';
+ }
+ $placeholder = array_key_exists('placeholder', $attrs) ? $attrs['placeholder'] : 'Search...';
+
+ $buttonText = array_key_exists('buttonText', $attrs) && !empty($attrs['buttonText']) ? $attrs['buttonText'] : '';
+
+ $isInside = array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-inside';
+
+ $hideInput = $this->checkAttrs('isSearchFieldHidden', $attrs) || (array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-only');
+
+ return str_replace('<div class="search-container row left nowrap"', sprintf(
+ '<div%s',
+ $this->getClassesAndStyles($attrs, ['search-container', 'row', 'left', 'nowrap'])
+ ), jvbSearch($placeholder, uniqid(), $label, $buttonText, $isInside, $hideInput));
+ }
//core_shortcode
public function prerender_core_social_link(array $block, ?string $content, ?WP_Block $parent):?string
{
// jvbDump($block, 'social link');
// jvbDump($parent, 'Parent');
-
+ $parentAttrs = false;
+ if ($parent) {
+ $parentAttrs = $parent->attributes;
+ }
$attrs = $block['attrs']??[];
$url = $attrs['url']??'';
$service = $attrs['service']?:'';
@@ -1172,16 +2045,100 @@
if (!$icon) {
$icon = jvbIcon('link');
}
- 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>';
+ $serviceName = $this->getServiceName($service);
+ $label = $parentAttrs && (!array_key_exists('className', $parentAttrs) || !str_contains($parentAttrs['className'], 'logos-only'))
+ ? sprintf(
+ '<span>%s</span>',
+ $serviceName
+ )
+ : sprintf(
+ '<span class="screen-reader-text">Find us on %s</span>',
+ $serviceName
+ );
+ $pillShaped = $parentAttrs && (array_key_exists('className', $parentAttrs) && str_contains($parentAttrs['className'], 'pill-shape'))
+ ? 'style="border-radius:var(--radius-outer);"'
+ : '';
+ return sprintf(
+ '<li><a href="%s" target="_blank" rel="nofollow" title="Find us on %s"%s>%s%s</a></li>',
+ $url,
+ $serviceName,
+ $pillShaped,
+ $icon,
+ $label
+ );
}
+ private function getServiceName(string $service) {
+ return match($service){
+ 'wordpress' => 'WordPress',
+ default => ucfirst($service)
+ };
+ }
public function prerender_core_social_links(array $block, ?string $content, ?WP_Block $parent):?string
{
-// jvbDump($block, 'social links');
+// jvbDump($block['attrs']??[], 'social links');
// jvbDump($parent, 'Parent');
- return '<ul class="socials">'.$this->innerBlocks($block).'</ul>';
+ return sprintf(
+ '<ul%s>%s</ul>',
+ $this->getClassesAndStyles($block['attrs']??[], ['socials']),
+ $this->innerBlocks($block, '','',$block)
+ );
}
//core_tag_cloud
+ public function prerender_core_tag_cloud(array $block, ?string $content, ?WP_Block $parent):?string
+ {
+// jvbDump($block, 'tag cloud');
+ $attrs = $block['attrs']??[];
+ $taxonomy = (array_key_exists('taxonomy', $attrs) && !empty($attrs['taxonomy']))
+ ? $attrs['taxonomy']
+ : 'post_tag';
+ $showCounts = $this->checkAttrs('showTagCounts', $attrs);
+
+ $terms = get_terms([
+ 'taxonomy' => $taxonomy,
+ 'hide_empty' => true,
+ ]);
+
+ if (!$terms || is_wp_error($terms)) {
+ return '';
+ }
+
+ $inside = '';
+
+ foreach ($terms as $term) {
+ $url = get_term_link($term->term_id, $taxonomy);
+ $count = $showCounts ?
+ sprintf(
+ '<span class="count">%d</span>',
+ $term->count
+ ) :
+ '';
+ $size = match(true) {
+ $term->count <= 2 => 'small',
+ $term->count <= 5 => 'x-small',
+ $term->count <= 10 => 'medium',
+ $term->count <= 15 => 'x-medium',
+ $term->count <= 20 => 'large',
+ $term->count <= 25 => 'x-large',
+ $term->count <= 30 => 'xx-large',
+ $term->count > 30 => 'xxx-large',
+ };
+ $fontSize = 'font-size: var(--txt-'.$size.');';
+ $inside .= sprintf(
+ '<li class="%s");"><a href="%s" rel="tag">%s%s</a></li>',
+ $size,
+// $fontSize,
+ $url,
+ $term->name,
+ $count
+ );
+ }
+ return sprintf(
+ '<ul%s>%s</ul>',
+ $this->getClassesAndStyles($attrs, ['term-list','cloud', jvbNoBase($taxonomy)]),
+ $inside
+ );
+ }
/**
@@ -1208,13 +2165,21 @@
return trim($clean);
}
- public function innerBlocks(array $block, string $before = '', string $after = '', bool $prerender = true):string
+ public function innerBlocks(array $block, string $before = '', string $after = '', ?array $parent = null):string
{
+ if ($parent) {
+ $parent = new WP_Block($parent);
+ }
$content = '';
foreach ($block['innerBlocks'] as $b) {
+
+ $rendered = $parent
+ ? $this->checkMethods(null, $b, $parent, true)
+ : render_block($b);
+
$content .= sprintf('%s%s%s',
$before,
- render_block($b),
+ $rendered,
$after
);
}
@@ -1392,15 +2357,36 @@
$classes[] = 'col';
}
+
// Merge with passed classes and styles
$styles = array_merge($attr_styles, $styles);
$classes = array_merge($attr_classes, $classes);
+ if (!empty(static::$pendingClass)) {
+ $classes = array_merge($classes, static::$pendingClass);
+ static::$pendingClass = [];
+ }
+ $classes = array_unique($classes);
+ $data = $this->getDataset($attrs);
+
// Build attribute strings
$class_string = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : '';
$style_string = !empty($styles) ? ' style="' . implode(';', $styles) . '"' : '';
+ $data_string = '';
+ if (!empty($data)) {
+ foreach ($data as $d => $v) {
+ if ($d === 'bg-small') {
+ $data_string .= ' data-bg-img';
+ }
+ $data_string .= sprintf(
+ ' data-%s="%s"',
+ $d,
+ $v
+ );
+ }
+ }
- $return = trim($class_string . $style_string);
+ $return = trim($class_string . $style_string . $data_string);
return ($return=='')? '' : ' '.$return;
}
/**
@@ -1430,18 +2416,18 @@
$classes = [];
foreach ($attrs as $key => $value) {
$class = $this->getClass($key, $value, $attrs);
- if (is_array($class)) {
- $classes = array_merge($classes, $class);
- } else {
- $classes[] = $class;
- }
+ if (is_string($class)) {
+ $class = explode(' ', $class);
+ }
+ $classes = array_merge($classes, $class);
}
- return array_filter($classes, function ($class) {
+ return array_unique(array_filter($classes, function ($class) {
return $class!=='' && !str_starts_with($class, 'wp');
- });
+ }));
}
protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array
{
+ //TODO: gradient
switch ($key) {
//Any additional classes the user adds
case 'className':
@@ -1451,112 +2437,38 @@
default => str_replace('is-style-', '', $value),
};
case 'contentPosition':
-
- $classes = [];
- $pos = explode(' ', $value);
- foreach($pos as $p) {
- switch ($p) {
- case 'top':
- $classes[] = 'a-start';
- break;
- case 'right':
- $classes[] = 'end';
- break;
- case 'bottom':
- $classes[] = 'a-end';
- break;
- case 'left':
- $classes[] = 'start';
- break;
- }
- }
- return implode(' ', $classes);
+ return $this->getContentPosition($value);
+ case 'term':
+ case 'taxonomy':
+ return jvbNoBase($value);
//Layout attributes
case 'layout':
- $classes = [];
- $type = 'row';
- if (array_key_exists('type', $value)) {
- $type = 'col';
-// if ($value['type'] === 'constrained') {
-// $classes[] = 'container col';
-// }
- }
- if (array_key_exists('orientation', $value)) {
- $type = 'col';
- if ($value['orientation'] === 'vertical') {
- $classes[] = 'col';
- if (in_array('row', $classes)) {
- $index = array_search('row', $classes);
- unset($classes[$index]);
- }
- }
- }else if (array_key_exists('type', $value) && $value['type'] === 'flex') {
- $classes[] = 'row';
- if (in_array('col', $classes)) {
- $index = array_search('col', $classes);
- unset($classes[$index]);
- }
- }
-//jvbDump($type);
-//jvbDump($value);
-// $check = [$value, $attrs];
-// foreach ($check as $ch) {
-//
-// }
- if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
- $classes[] = ($type === 'row') ? 'start' : 'a-start';
- }
- if (array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
- if (in_array($value['justifyContent'], ['left', 'right','space-between'])) {
-// jvbDump($type);
- switch ($value['justifyContent']) {
- case 'right':
- $classes[] = ($type === 'row') ? 'end' : 'a-end';
- break;
- case 'space-between':
- $classes[] = 'btw';
- break;
- }
- }
- }
-
-
- if (array_key_exists('flexWrap', $value)) {
- if ($value['flexWrap'] === 'nowrap') {
- $classes[] = 'nowrap';
- }
- }
- return implode(' ', $classes);
+ return $this->getLayout($value, $attrs);
case 'align':
return !empty($value) ? 'align-'.$value : '';
case 'verticalAlignment':
- return !empty($value) ? 'v-align-'.$value : '';
- case 'isStackedMobile':
+ switch ($value) {
+ case 'bottom':
+ $value = 'btm';
+ break;
+ case 'center':
+ $value = 'y-mid';
+ default:
+ }
+ return !empty($value) ? $value : '';
+ case 'isStackedOnMobile':
return ($value === true) ? 'stack-small' : '';
case 'justifyContent':
return !empty($value) ? 'j-'.$value : '';
case 'orientation':
return $value==='column' ? 'column' : '';
case 'width':
+ return $this->getWidth($value);
case 'dimRatio':
- if (is_numeric($value)) {
- $width = match (true) {
- $value < 25 => '25',
- $value < 33 => '33',
- $value <= 50 => '50',
- $value < 66 => '66',
- $value < 75 => '75',
- default => 'full',
- };
- switch ($key) {
- case 'width':
- return 'width-'.$width;
- case 'dimRatio':
- return 'overlay-'.$width;
- }
- }
- return '';
-
+ return $this->getDimRatio($value);
+ case 'overlayColor':
+ return $value;
+ break;
//Typography
case 'textAlign':
return !empty($value) ? 'text-'.$value : '';
@@ -1571,171 +2483,285 @@
//Style base:
case 'style':
- $classes = [];
- //Margin and Padding
- if (array_key_exists('spacing', $value)) {
- foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
- if (array_key_exists($search, $value['spacing'])) {
- $directions = [];
-
- // Collect ONLY preset spacing values for classes
- foreach ($value['spacing'][$search] as $direction => $size) {
- $presetSize = $this->getPresetSpacing($size);
- if ($presetSize) {
- $directions[$direction] = $presetSize;
- }
- // Non-preset values are skipped here and handled by inline styles below
- }
-
- if (empty($directions)) {
- continue;
- }
-
- // Check what directions we have
- $hasTop = isset($directions['top']);
- $hasBottom = isset($directions['bottom']);
- $hasLeft = isset($directions['left']);
- $hasRight = isset($directions['right']);
-
- // Check if axes match
- $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
- $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
-
- // All 4 directions exist and match → p-3
- if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
- count(array_unique($directions)) === 1) {
- $classes[] = $c . '-' . reset($directions);
- }
- // Both axes match → px-3 py-2
- elseif ($xMatch && $yMatch) {
- $classes[] = $c . 'x-' . $directions['left'];
- $classes[] = $c . 'y-' . $directions['top'];
- }
- // Only X axis matches → px-3 (+ individual for top/bottom)
- elseif ($xMatch) {
- $classes[] = $c . 'x-' . $directions['left'];
- if ($hasTop) {
- $classes[] = $c . 't-' . $directions['top'];
- }
- if ($hasBottom) {
- $classes[] = $c . 'b-' . $directions['bottom'];
- }
- }
- // Only Y axis matches → py-3 (+ individual for left/right)
- elseif ($yMatch) {
- $classes[] = $c . 'y-' . $directions['top'];
- if ($hasLeft) {
- $classes[] = $c . 'l-' . $directions['left'];
- }
- if ($hasRight) {
- $classes[] = $c . 'r-' . $directions['right'];
- }
- }
- // No matches - individual directions
- else {
- foreach ($directions as $direction => $size) {
- $dir = match($direction) {
- 'top' => 't',
- 'bottom' => 'b',
- 'left' => 'l',
- 'right' => 'r',
- default => $direction
- };
- $classes[] = $c . $dir . '-' . $size;
- }
- }
- }
- }
- }
-
- if (array_key_exists('fontSize', $value)) {
- if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
- $classes[] = 'font-'.$value['fontSize'];
- }
- if (in_array('fontWeight', $value)) {
- $classes[] = 'text-'.$value['fontWeight'];
- }
- if (in_array('textTransform', $value)) {
- if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
- $classes[] = $value['textTransform'];
- }
- }
- }
- return implode(' ', $classes);
+ return $this->getPresetStyles($value);
case 'fontSize':
$classes[] = 'font-'.$value;
return implode(' ', $classes);
- case 'isStackedOnMobile':
- return ($value === true) ? 'stack-small' : '';
- case 'width':
- if (is_numeric($value)) {
- $width = match (true) {
- $value < 25 => '25',
- $value < 33 => '33',
- $value <= 50 => '50',
- $value < 66 => '66',
- $value < 75 => '75',
- default => 'full',
- };
- switch ($key) {
- case 'width':
- return 'width-'.$width;
- case 'dimRatio':
- return 'overlay-'.$width;
- }
+ case 'postLayout':
+ $classes[] = 'item-grid';
+ if (isset($attrs['columns']) && $attrs['columns']!== 3){
+ $classes[] = sprintf(
+ 'split-%d',
+ $attrs['columns']
+ );
}
- return '';
+ return $classes;
default:
- $ignore = [
- 'useFeaturedImage',
- 'opacity',
- 'borderColor',
- 'backgroundColor',
- 'textColor',
- 'minHeight',
- 'minHeightUnit',
- 'isDark',
- 'sizeSlug',
- 'isUserOverlayColor',
- 'customOverlayColor',
- 'dimRatio',
- 'placeholder',
- 'alt',
- 'imageFill',
- 'mediaSizeSlug',
- 'isLink',
- 'kind',
- 'label',
- 'type',
- 'id',
- 'url',
- 'label',
- 'shouldSyncIcon',
- 'rel',
- 'opensInNewTab',
- 'title',
- 'ref',
- 'overlayMenu',
- 'slug',
- 'theme',
- 'tagName',
- 'level',
- 'ordered',
- 'area',
- 'mediaId',
- 'mediaLink',
- 'mediaType',
- 'height', //maybe still need?
- ];
- if (!is_admin() &&!in_array($key, $ignore)) {
+ if (JVB_TESTING && !is_admin() &&!in_array($key, $this->ignore)) {
// TESTING
-// jvbDump($key, 'getClass');
-// jvbDump($attrs);
+ jvbDump($attrs, '[getClass] '.$key);
}
return '';
}
}
+ /*** CLASS HELPERS ***/
+ private function getContentPosition(string $value):string
+ {
+ $classes = [];
+ $pos = explode(' ', $value);
+ foreach($pos as $p) {
+ switch ($p) {
+ case 'top':
+ $classes[] = 'top';
+ break;
+ case 'right':
+ $classes[] = 'right';
+ break;
+ case 'bottom':
+ $classes[] = 'btm';
+ break;
+ case 'left':
+ $classes[] = 'left';
+ break;
+ }
+ }
+ return implode(' ', $classes);
+ }
+
+ private function getLayout(array $value, array $attrs):array
+ {
+// jvbDump($value, 'getLayout');
+ $classes = [];
+
+ $type = 'row';
+ $isRow = true;
+ //Determine type
+ if ((array_key_exists('type', $value) && !in_array($value['type'], ['flex', 'grid'])) ||
+ (array_key_exists('orientation', $value) && $value['orientation'] === 'vertical')) {
+ $type = 'col';
+ $isRow = false;
+ } elseif (array_key_exists('type', $value) && $value['type'] === 'grid') {
+ $type = 'item-grid';
+ $isRow = false;
+ if (array_key_exists('columnCount', $value) && $value['columnCount']!== 3) {
+ $classes[] = sprintf(
+ 'split-%s',
+ $value['columnCount']
+ );
+ }
+ }
+
+ if (array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
+ switch ($value['justifyContent']) {
+ case 'right':
+ $classes[] = 'right';
+ break;
+ case 'center':
+ $classes[] = 'x-mid';
+ break;
+ case 'space-between':
+ $classes[] = 'x-btw';
+ break;
+ case 'left':
+ $classes[] = 'left';
+ break;
+ case 'space-evenly':
+ $classes[] = 'x-even';
+ break;
+ case 'space-around':
+ $classes[] = 'x-around';
+ break;
+ case 'stretch':
+ $classes[] = 'stretch';
+ }
+ } else {
+ $classes[] = 'left';
+ }
+
+ if (array_key_exists('verticalAlignment', $value)) {
+ switch ($value['verticalAlignment']) {
+ case 'bottom':
+ $classes[] = 'btm';
+ break;
+ case 'top':
+ $classes[] = 'top';
+ break;
+ case 'center':
+ $classes[] = 'y-mid';
+ break;
+ case 'space-between':
+ $classes[] = 'y-btw';
+ break;
+ case 'space-around':
+ $classes[] = 'y-around';
+ break;
+ case 'space-even':
+ $classes[] = 'y-even';
+ }
+ }
+
+
+
+ if (array_key_exists('flexWrap', $value)) {
+ if ($value['flexWrap'] === 'nowrap') {
+ $classes[] = 'nowrap';
+ }
+ }
+
+ $classes[] = $type;
+ return $classes;
+ }
+
+ private function getWidth(string $value):string
+ {
+ $value = (int)str_replace('%', '', $value);
+ return sprintf(
+ 'width-%d',
+ match (true) {
+ $value <= 25 => '25',
+ $value <= 33 => '33',
+ $value <= 50 => '50',
+ $value <= 66 => '66',
+ $value <= 75 => '75',
+ default => 'full',
+ }
+ );
+ }
+ private function getDimRatio(string $value):string
+ {
+ if (is_numeric($value)) {
+ return sprintf(
+ 'op-%d',
+ match (true) {
+ $value <= 14 => '1',
+ $value <= 28 => '2',
+ $value <= 42 => '3',
+ $value <= 56 => '45',
+ $value <= 70 => '4',
+ $value <= 84 => '5',
+ default => '6',
+ }
+ );
+ }
+ return '';
+ }
+
+ private function getPresetStyles(array $value):string
+ {
+ $classes = [];
+ //Margin and Padding
+ if (array_key_exists('spacing', $value)) {
+ $classes = array_merge($classes, $this->buildSpacingClasses($value));
+ }
+ if (array_key_exists('color', $value)) {
+ if (array_key_exists('duotone', $value['color'])) {
+ $preset = explode('|', $value['color']['duotone']);
+ $preset = $preset[array_key_last($preset)];
+ if (str_contains($preset, '-')) {
+ $preset = explode('-', $preset);
+ } else {
+ $preset = [$preset];
+ }
+ $classes[] = 'duotone';
+ foreach ($preset as $p) {
+ $classes[] = $p;
+ }
+ }
+ }
+
+ if (array_key_exists('fontSize', $value)) {
+ if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
+ $classes[] = 'font-'.$value['fontSize'];
+ }
+ if (in_array('fontWeight', $value)) {
+ $classes[] = 'text-'.$value['fontWeight'];
+ }
+ if (in_array('textTransform', $value)) {
+ if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
+ $classes[] = $value['textTransform'];
+ }
+ }
+ }
+ return implode(' ', $classes);
+ }
+ private function buildSpacingClasses(array $value):array
+ {
+ $classes = [];
+ foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
+ if (array_key_exists($search, $value['spacing'])) {
+ $directions = [];
+
+ // Collect ONLY preset spacing values for classes
+ foreach ($value['spacing'][$search] as $direction => $size) {
+ $presetSize = $this->getPresetSpacing($size);
+ if ($presetSize) {
+ $directions[$direction] = $presetSize;
+ }
+ // Non-preset values are skipped here and handled by inline styles below
+ }
+
+ if (empty($directions)) {
+ continue;
+ }
+
+ // Check what directions we have
+ $hasTop = isset($directions['top']);
+ $hasBottom = isset($directions['bottom']);
+ $hasLeft = isset($directions['left']);
+ $hasRight = isset($directions['right']);
+
+ // Check if axes match
+ $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
+ $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
+
+ // All 4 directions exist and match → p-3
+ if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
+ count(array_unique($directions)) === 1) {
+ $classes[] = $c . '-' . reset($directions);
+ }
+ // Both axes match → px-3 py-2
+ elseif ($xMatch && $yMatch) {
+ $classes[] = $c . 'x-' . $directions['left'];
+ $classes[] = $c . 'y-' . $directions['top'];
+ }
+ // Only X axis matches → px-3 (+ individual for top/bottom)
+ elseif ($xMatch) {
+ $classes[] = $c . 'x-' . $directions['left'];
+ if ($hasTop) {
+ $classes[] = $c . 't-' . $directions['top'];
+ }
+ if ($hasBottom) {
+ $classes[] = $c . 'b-' . $directions['bottom'];
+ }
+ }
+ // Only Y axis matches → py-3 (+ individual for left/right)
+ elseif ($yMatch) {
+ $classes[] = $c . 'y-' . $directions['top'];
+ if ($hasLeft) {
+ $classes[] = $c . 'l-' . $directions['left'];
+ }
+ if ($hasRight) {
+ $classes[] = $c . 'r-' . $directions['right'];
+ }
+ }
+ // No matches - individual directions
+ else {
+ foreach ($directions as $direction => $size) {
+ $dir = match($direction) {
+ 'top' => 't',
+ 'bottom' => 'b',
+ 'left' => 'l',
+ 'right' => 'r',
+ default => $direction
+ };
+ $classes[] = $c . $dir . '-' . $size;
+ }
+ }
+ }
+ }
+ return $classes;
+ }
protected function getInlineStyles(array $attrs):array
{
@@ -1745,258 +2771,74 @@
$styles = [];
foreach ($attrs as $key => $value) {
$style = $this->getStyle($key, $value, $attrs);
+ if (is_string($style)) {
+ $style = [$style];
+ }
$styles = array_merge($styles, $style);
}
return $styles;
}
- protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array
+ protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array|string
{
$styles = [];
switch ($key) {
// Font family settings
- case 'fontFamily':
- if ($value === 'body') {
- $styles[] = 'font-family: "Open Sans", system-ui, -apple-system, sans-serif';
- } elseif ($value === 'heading') {
- $styles[] = 'font-family: "Josefin Sans", system-ui, -apple-system, sans-serif';
- } elseif (!empty($value)) {
- $styles[] = 'font-family: '.$value;
- }
- break;
+ case 'size':
+ return $this->getIconSizeStyle($value);
+
+ case 'fontFamily':
+ return $this->getFontFamilyStyle($value);
// Icon color (for icon blocks)
case 'iconColorValue':
- if (!empty($value)) {
- $styles[] = 'color: '.$value;
- }
- break;
+ return $this->getColorStyle($value);
// Minimum height settings
case 'minHeight':
- if (!empty($value) && isset($attrs['minHeightUnit'])) {
- $styles[] = 'min-height: '.$value.$attrs['minHeightUnit'];
- } elseif (!empty($value)) {
- $styles[] = 'min-height: '.$value.'px'; // Default to px if no unit specified
- }
- break;
+ return $this->getMinHeightStyle($value, $attrs);
// Background URL (for cover, media blocks)
case 'url':
if (!empty($value) && str_starts_with($value, 'http')) {
- $styles[] = 'background-image: url('.$value.')';
+ return 'background-image: url('.$value.')';
}
break;
// Focal point for background images
case 'focalPoint':
- $x = (array_key_exists('x', $attrs['focalPoint'])) ? $attrs['focalPoint']['x'] * 100 : 'center';
- $y = (array_key_exists('y', $attrs['focalPoint'])) ? $attrs['focalPoint']['y'] * 100 : 'center';
- $styles[] = 'background-position:'.$x.' '.$y.';';
-
- break;
+ return $this->getFocalPointStyle($value);
// Complex style object
case 'style':
- // Border styles
- if (isset($value['border'])) {
- $border = $value['border'];
+ return $this->extractStyles($value, $attrs);
- if (isset($border['radius'])) {
- $styles[] = 'border-radius: '.$border['radius'];
- }
-
- if (isset($border['width'])) {
- $styles[] = 'border-width: '.$border['width'];
- }
-
- if (isset($border['style']) && isset($border['width']) && !empty($border['style'])) {
- $styles[] = 'border-style: '.$border['style'];
- }
-
- if (isset($border['color'])) {
- $styles[] = 'border-color: '.$border['color'];
- }
- }
-
- // Color styles
- if (isset($value['color'])) {
- $color = $value['color'];
-
- if (isset($color['background'])) {
- $styles[] = 'background-color: '.$color['background'];
- }
-
- if (isset($color['text'])) {
- $styles[] = 'color: '.$color['text'];
- }
-
- if (isset($color['gradient'])) {
- $styles[] = 'background: '.$color['gradient'];
- }
- }
-
- // Layout styles
- if (isset($value['layout'])) {
- foreach ($value['layout'] as $layout => $option) {
- switch ($layout) {
- case 'selfStretch':
- if ($option === 'fixed' && isset($value['layout']['selfStretchValue'])) {
- $styles[] = 'width: '.$value['layout']['selfStretchValue'];
- }
- break;
- }
- }
- }
-
- // Typography styles
- if (isset($value['typography'])) {
- $typography = $value['typography'];
-
- if (isset($typography['fontSize'])) {
- $styles[] = 'font-size: '.$typography['fontSize'];
- }
-
- if (isset($typography['fontWeight'])) {
- $styles[] = 'font-weight: '.$typography['fontWeight'];
- }
-
- if (isset($typography['textDecoration'])) {
- $styles[] = 'text-decoration: '.$typography['textDecoration'];
- }
-
- if (isset($typography['textTransform'])) {
- $styles[] = 'text-transform: '.$typography['textTransform'];
- }
-
- if (isset($typography['letterSpacing'])) {
- $styles[] = 'letter-spacing: '.$typography['letterSpacing'];
- }
-
- if (isset($typography['lineHeight'])) {
- $styles[] = 'line-height: '.$typography['lineHeight'];
- }
- }
-
- // Spacing styles
- if (isset($value['spacing'])) {
- $spacing = $value['spacing'];
-
- // Don't duplicate margin/padding that's handled by classes
- // Only add specific CSS values here that wouldn't work well as classes
- if (isset($spacing['margin'])) {
- foreach ($spacing['margin'] as $direction => $size) {
- // If not a preset value, add as inline style
- if (!str_contains($size, 'var:preset')) {
- $styles[] = 'margin-'.$direction.': '.$size;
- }
- }
- }
-
- if (isset($spacing['padding'])) {
- foreach ($spacing['padding'] as $direction => $size) {
- // If not a preset value, add as inline style
- if (!str_contains($size, 'var:preset')) {
- $styles[] = 'padding-'.$direction.': '.$size;
- }
- }
- }
- }
- break;
case 'dimRatio':
- $ratio = (ceil($value /25) *25);
- $s = 'background-color: rgba(var(--base-rgb), ';
- switch ($ratio) {
- case 0:
- $s .= 'var(--rgb-subtle-hover));';
- break;
- case 25:
- $s .= 'var(--rgb-light));';
- break;
- case 50:
- $s .= 'var(--rgb-medium));';
- break;
- default:
- $s .= 'var(--rgb-heavy));';
- break;
- }
- $styles[] = $s;
- break;
+ return $this->getDimRatioStyle($value, $attrs);
// Custom styles (any other attributes that need inline styling)
case 'backgroundType':
if ($value === 'video' && isset($attrs['backgroundUrl'])) {
// Don't set a background image for videos - it will be handled by the video element
} elseif (isset($attrs['backgroundUrl'])) {
- $styles[] = 'background-image: url('.$attrs['backgroundUrl'].')';
+ return 'background-image: url('.$attrs['backgroundUrl'].')';
}
break;
case 'backgroundColor':
case 'borderColor':
case 'textColor':
- $type = ($key === 'backgroundColor') ? 'background-color:' : (($key === 'borderColor') ? 'border-color:' : 'color:');
- $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
- $continue = true;
- foreach ($defaults as $default) {
- if (str_starts_with($value, $default)) {
- $continue = false;
- $styles[] = $type.'var(--'.$value.')';
- }
+ $type = str_replace('Color', '-color', $key);
+ $type = str_replace('text-', '', $type);
+ if (isset($attrs['border']['width']) && $key === 'borderColor') {
+ break;
}
- if ($continue) {
- $styles[] = $type.$value;
- }
- break;
+ return $this->getColorStyle($value, $type);
+
// Any other attributes that need direct styling
default:
- $ignore = [
- 'useFeaturedImage',
- 'opacity',
- 'textAlign',
- 'minHeightUnit',
- 'isDark',
- 'isUserOverlayColor',
- 'contentPosition',
- 'sizeSlug',
- 'customOverlayColor',
- 'alt',
- 'placeholder',
- 'imageFill',
- 'mediaSizeSlug',
- 'isLink',
- 'kind',
- 'label',
- 'type',
- 'id',
- 'url',
- 'label',
- 'shouldSyncIcon',
- 'rel',
- 'opensInNewTab',
- 'title',
- 'ref',
- 'overlayMenu',
- 'slug',
- 'theme',
- 'tagName',
- 'level',
- 'ordered',
- 'area',
- 'className',
- 'fontSize',
- 'layout',
- 'align',
- 'mediaId',
- 'mediaLink',
- 'mediaType',
- 'isStackedOnMobile',
- 'width',
- 'height', // maybe still need?
- ];
- if (!is_admin() && !in_array($key, $ignore)) {
+ if (JVB_TESTING && !is_admin() && !in_array($key, $this->ignore)) {
//TESTING
-// jvbDump($key, 'getStyle');
-// jvbDump($attrs);
+ jvbDump($attrs, '[getStyle] '.$key);
}
// No default inline styles
break;
@@ -2004,6 +2846,367 @@
return $styles;
}
+ private function getIconSizeStyle(string $value):array
+ {
+ $values = explode(' ', $value);
+ $styles = [];
+ foreach ($values as $v) {
+ switch ($value) {
+ case 'has-small-icon-size':
+ $styles[] = '--w:var(--txt-x-small)';
+ break;
+ case 'has-large-icon-size':
+ $styles[] = '--w:var(--txt-large)';
+ break;
+ case 'has-huge-icon-size':
+ $styles[] = '--w:var(--txt-xx-large)';
+ break;
+ default:
+ if (JVB_TESTING) {
+ jvbDump($value, 'No preset found for size: '.print_r($value, true));
+ }
+ }
+ }
+ return $styles;
+ }
+ private function getFontFamilyStyle(string $value):string
+ {
+ return match($value) {
+ 'body' => 'font-family: var(--body)',
+ 'heading' => 'font-family: var(--heading)',
+ default => sprintf('font-family: %s', $value)
+ };
+ }
+ private function getColorStyle(string $value, ?string $type = 'color'):string
+ {
+ if (!in_array($type, ['color', 'background','background-color','border-color'])) {
+ $type = null;
+ }
+ return sprintf(
+ '%s%s',
+ $type ? $type.': ' : '',
+ $this->getColor($value)
+ );
+ }
+ private function getMinHeightStyle(string $value, array $attrs):string
+ {
+ $out = '';
+ if (!empty($value)) {
+ if (isset($attrs['minHeightUnit'])) {
+ $out = sprintf(
+ 'min-height: %s%s',
+ $value,
+ $attrs['minHeightUnit']
+ );
+ } else {
+ $out = sprintf(
+ 'min-height: %spx',
+ $value
+ );
+ }
+ }
+ return $out;
+ }
+
+ private function getFocalPointStyle(array $value):string
+ {
+ jvbDump($value, 'Focal Point');
+ $x = array_key_exists('x', $value) ? $value['x'] * 100 : 'center';
+ $y = array_key_exists('y', $value) ? $value['y'] * 100 : 'center';
+
+ $y = $x === $y ? '' : ' '.$y;
+ return sprintf(
+ 'background-position:%s%s',
+ $x,
+ $y
+ );
+ }
+
+ private function extractStyles(array $value, array $attrs):array
+ {
+ $styles = [];
+ foreach ($value as $k => $v) {
+ switch ($k) {
+ case 'border':
+ $styles = array_merge($styles, $this->getBorderStyle($v, $attrs));
+ break;
+
+ case 'color':
+ if (isset($v['background'])) {
+ $styles[] = $this->getColorStyle($v['background'], 'background-color');
+ }
+ if (isset($v['text'])) {
+ $styles[] = $this->getColorStyle($v['text']);
+ }
+ if (isset($v['gradient'])) {
+ jvbDump($v, 'Gradient');
+ }
+ break;
+
+ case 'layout':
+ $styles = array_merge($styles, $this->getLayoutStyle($v, $attrs));
+ break;
+
+ case 'typography':
+ $styles = array_merge($styles, $this->getTypographyStyle($v, $attrs));
+ break;
+
+ case 'spacing':
+ if (isset($v['blockGap'])) {
+ if (is_array($v['blockGap'])) {
+ $inner = [];
+ foreach ($v['blockGap'] as $gap) {
+ $inner[] = sprintf(
+ 'var(--sp%s)',
+ $this->getPresetSpacing($gap)
+ );
+ }
+ if (!empty($inner)) {
+ $styles[] = sprintf(
+ '--gap: %s',
+ implode(' ', $inner)
+ );
+ }
+ } else {
+ $styles[] = '--gap: var(--sp'.$this->getPresetSpacing($v['blockGap']).')';
+ }
+ }
+
+ // Don't duplicate margin/padding that's handled by classes
+ // Only add specific CSS values here that wouldn't work well as classes
+ if (isset($v['margin'])) {
+ foreach ($v['margin'] as $direction => $size) {
+ if (!str_contains($size, 'var:preset')) {
+ $styles[] = 'margin-'.$direction.': '.$size;
+ }
+ }
+ }
+
+ if (isset($v['padding'])) {
+ foreach($v['padding'] as $dir => $size) {
+ if (!str_contains($size, 'var:preset')) {
+ $styles[] = 'padding-'.$dir.': '.$size;
+ }
+ }
+ }
+ break;
+
+ case 'background':
+ if (array_key_exists('backgroundImage', $v)) {
+ $data = Image::getData($v['backgroundImage']['id']);
+ if (!empty($data) && array_key_exists('tiny', $data)) {
+ $styles[] = sprintf(
+ 'background-image: url(%s)',
+ $data['tiny']
+ );
+ }
+
+ }
+ break;
+
+ case 'dimensions':
+ foreach ($v as $sk => $sv) {
+ if ($sk === 'minHeight') {
+ $styles[] = sprintf(
+ 'min-height: %s',
+ $sv
+ );
+ } else {
+ jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true));
+ }
+ }
+
+
+ break;
+
+ case 'elements':
+ if (!empty($v)) {
+ // Generate a unique class tied to this block instance
+ $uid = 'b-'.substr(md5(serialize($attrs)), 0, 8);
+ $this->extractElementStyles($v, $uid);
+ // We need the uid added as a class — store it for getClassesAndStyles to pick up
+ static::$pendingClass[] = $uid;
+ }
+ break;
+
+ default:
+ jvbDump('No config set for '.$k.': '.print_r($v, true));
+ }
+ }
+
+ return $styles;
+ }
+ private function getBorderStyle(array $border, array $attrs):array
+ {
+ $styles = [];
+
+ if (isset($border['radius'])) {
+ $styles[] = 'border-radius: '.$border['radius'];
+ }
+
+ if (isset($border['width']) && isset($attrs['borderColor'])) {
+ $st = $border['style'] ?? 'solid';
+ $styles[] = sprintf(
+ 'border: %s %s %s',
+ $border['width'],
+ $st,
+ $this->getColor($attrs['borderColor'])
+ );
+ } elseif (isset($border['width'])) {
+ $styles[] = 'border-width: '.$border['width'];
+ } elseif (isset($border['style'])) {
+ $styles[] = 'border-style: '.$border['style'];
+ }
+
+ return $styles;
+ }
+
+ private function getLayoutStyle(array $layout, array $attrs):array
+ {
+ $styles = [];
+// jvbDump($layout);
+
+ foreach ($layout as $l => $option) {
+ switch ($l) {
+ case 'selfStretch':
+ if ($option === 'fixed' && isset($layout['selfStretchValue'])) {
+ $styles[] = 'width: '.$layout['selfStretchValue'];
+ } elseif ($option === 'fill') {
+ $styles[] = 'flex:1';
+ }
+ break;
+ default:
+ $ignore = [
+ 'selfStretchValue',
+ 'flexSize',
+ ];
+ if (JVB_TESTING && !in_array($l, $ignore)) {
+ jvbDump($l, 'No layout style set for: ');
+ }
+// case 'type':
+// if ($option === 'grid' && $value['layout']['columnCount'] !== 3) {
+// $styles[] = sprintf(
+// 'grid-template-columns: repeat(1fr, %s)',
+// $value['layout']['columnCount']
+// );
+// }
+// break;
+ }
+ }
+ return $styles;
+ }
+
+ private function getTypographyStyle(array $typography, array $attrs):array
+ {
+ $styles = [];
+
+ if (isset($typography['fontSize'])) {
+ $styles[] = 'font-size: '.$typography['fontSize'];
+ }
+
+ if (isset($typography['fontWeight'])) {
+ $styles[] = 'font-weight: '.$typography['fontWeight'];
+ }
+
+ if (isset($typography['textDecoration'])) {
+ $styles[] = 'text-decoration: '.$typography['textDecoration'];
+ }
+
+ if (isset($typography['textTransform'])) {
+ $styles[] = 'text-transform: '.$typography['textTransform'];
+ }
+
+ if (isset($typography['letterSpacing'])) {
+ $styles[] = 'letter-spacing: '.$typography['letterSpacing'];
+ }
+
+ if (isset($typography['lineHeight'])) {
+ $styles[] = 'line-height: '.$typography['lineHeight'];
+ }
+ return $styles;
+ }
+ private function extractElementStyles(array $elements, string $uid):void
+ {
+ foreach ($elements as $element => $states) {
+ $selector = match($element) {
+ 'link' => "a",
+ 'heading' => "h1,h2,h3,h4,h5,h6",
+ 'button' => "button,.button",
+ default => $element,
+ };
+
+ foreach ($states as $state => $rules) {
+ $css = [];
+ $fullSelector = str_starts_with($state, ':')
+ ? ".{$uid} {$selector}{$state}"
+ : ".{$uid} {$selector}";
+
+ if (isset($rules['color']['text'])) {
+ $css[] = 'color: '.$this->getColor($rules['color']['text']);
+ }
+ if (isset($rules['color']['background'])) {
+ $css[] = 'background-color: '.$this->getColor($rules['color']['background']);
+ }
+ if (!empty($css)) {
+ static::$pendingStyles[] = $fullSelector.' { '.implode('; ', $css).' }';
+ }
+ }
+ }
+ }
+
+ public function maybeOutputCustomStyles(): string
+ {
+ if (empty(static::$pendingStyles)) return '';
+ $out = '<style>'.implode(' ', static::$pendingStyles).'</style>';
+ static::$pendingStyles = [];
+ return $out;
+ }
+
+ private function getDimRatioStyle(int $value, array $attrs):string
+ {
+ //TODO: This likely isn't working correctly
+ jvbDump($value, 'dimRatio');
+ jvbDump($attrs, 'dimRatio attrs');
+ $s = '';
+ if (!array_key_exists('overlayColor', $attrs)) {
+ $s = 'background-color: rgba(var(--base-rgb), ';
+ if ($value <= 14) {
+ $s .= 'var(--op-1));';
+ } elseif ($value <= 28) {
+ $s .= 'var(--op-2));';
+ } elseif ($value <= 42) {
+ $s .= 'var(--op-3));';
+ } elseif ($value <= 56) {
+ $s .= 'var(--op-45));';
+ } elseif ($value <= 70) {
+ $s .= 'var(--op-4));';
+ } elseif ($value <= 84) {
+ $s .= 'var(--op-5));';
+ } else {
+ $s .= 'var(--op-6));';
+ }
+ }
+ return $s;
+ }
+
+ protected function getDataset(array $attrs):array
+ {
+ $dataset = [];
+ if (array_key_exists('style', $attrs)) {
+ if (array_key_exists('background', $attrs['style'])){
+ if (array_key_exists('backgroundImage', $attrs['style']['background'])) {
+ $id = $attrs['style']['background']['backgroundImage']['id']??false;
+ if ($id) {
+ $data = Image::getData($id);
+ $dataset['bg-small'] = $data['small'];
+ $dataset['bg-med'] = $data['medium'];
+ $dataset['bg-large'] = $data['large'];
+ }
+ }
+ }
+ }
+ return $dataset;
+ }
public function formatImage(int $ID = 0, string $start = 'tiny', string $replace = 'large'):string
{
@@ -2016,4 +3219,28 @@
return jvbFormatImage($ID, $start, $replace);
}
+ protected function checkAttrs(string $test, array $attrs):bool
+ {
+ return array_key_exists($test, $attrs) && $attrs[$test]===true;
+ }
+
+ protected function getColor(string $value):string
+ {
+ $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
+ foreach ($defaults as $default) {
+ if (str_starts_with($value, $default)) {
+ return 'var(--'.$value.')';
+ }
+ }
+ return $value;
+ }
+
+ protected function counter(string $key):void
+ {
+ if (!array_key_exists($key, static::$counters)) {
+ static::$counters[$key] = 1;
+ } else {
+ static::$counters[$key]++;
+ }
+ }
}
diff --git a/inc/blocks/FeedBlock.php b/inc/blocks/FeedBlock.php
index fba0d1c..c6b1f0b 100644
--- a/inc/blocks/FeedBlock.php
+++ b/inc/blocks/FeedBlock.php
@@ -193,8 +193,8 @@
<?php if ($hasMany) {
//If we have multiple content, only show the content first
?>
- <details class="col a-start">
- <summary class="row btw">
+ <details class="col left">
+ <summary class="row x-btw">
<span class="label">SHOWING: </span>
<?php
$labels = [];
@@ -249,7 +249,7 @@
if (!empty ($this->config['taxonomies'])) {
?>
<div class="filters">
- <div class="filter-group row start">
+ <div class="filter-group row left">
<span class="label">FILTER BY:</span>
<?php
@@ -288,8 +288,8 @@
</div>
</div>
<?php } ?>
- <div class="row btw nowrap">
- <div class="order-by filter-group row start w-full">
+ <div class="row x-btw nowrap">
+ <div class="order-by filter-group row left w-full">
<span class="label">ORDER BY:</span>
<?php
//TODO: Get content types that can be sorted alphabetically
@@ -340,7 +340,7 @@
</div>
- <div class="order-direction filter-group row start w-full" data-for-order="date,modified,title<?= $custom === '' ? '' : ','.$custom?>">
+ <div class="order-direction filter-group row left w-full" data-for-order="date,modified,title<?= $custom === '' ? '' : ','.$custom?>">
<span class="label">ORDER:</span>
<input type="radio" id="order-desc" class="btn" name="order" value="desc" data-filter="order" checked>
<label for="order-desc" title="Sort Descending (A-Z, 1-10)" class="row">
diff --git a/inc/blocks/FormBlock.php b/inc/blocks/FormBlock.php
index 768ed18..0d447da 100644
--- a/inc/blocks/FormBlock.php
+++ b/inc/blocks/FormBlock.php
@@ -324,7 +324,7 @@
// Render navigation if multiple sections
if (count($sections) > 1) {
- echo '<nav class="tabs row start" role="tablist">';
+ echo '<nav class="tabs row left" role="tablist">';
$i = 1;
foreach ($sections as $slug => $section) {
$active_class = $i === 1 ? ' active' : '';
@@ -367,7 +367,7 @@
}
// Add step navigation buttons
- echo '<div class="step-navigation row btw">';
+ echo '<div class="step-navigation row x-btw">';
if ($i > 0) {
echo '<button type="button" class="button secondary prev-step" data-action="prev-step">';
diff --git a/inc/blocks/MenuBlock.php b/inc/blocks/MenuBlock.php
index 679f017..89b9c60 100644
--- a/inc/blocks/MenuBlock.php
+++ b/inc/blocks/MenuBlock.php
@@ -194,14 +194,14 @@
?>
<div class="menu-item<?= !empty($variations) ? ' variable' : '' ?>" data-section="<?=$slug?>">
- <div class="header row btw">
+ <div class="header row x-btw">
<h3><?= $values['post_title']?></h3>
<p class="price"><?= $priceRange ?></p>
</div>
<div class="description">
<?= Render::renderFrom($meta, 'post_excerpt')?>
</div>
- <div class="info row end">
+ <div class="info row right">
<?php
if (empty($variations)) {
Form::renderFrom($meta,
diff --git a/inc/blocks/SummaryBlock.php b/inc/blocks/SummaryBlock.php
index 5538f8a..ecf01ac 100644
--- a/inc/blocks/SummaryBlock.php
+++ b/inc/blocks/SummaryBlock.php
@@ -174,7 +174,7 @@
$open = $this->isOpen ? ' open' : '';
?>
<details class="info"<?=$open?>>
- <summary class="row btw"><?= $this->detailsTitle ?></summary>
+ <summary class="row x-btw"><?= $this->detailsTitle ?></summary>
<?php
foreach ($this->details as $key => $details) {
if ($details === '') {
diff --git a/inc/forms/TaxonomySelector.php b/inc/forms/TaxonomySelector.php
index 5af59da..5a8795a 100644
--- a/inc/forms/TaxonomySelector.php
+++ b/inc/forms/TaxonomySelector.php
@@ -111,8 +111,8 @@
<div class="items-wrap">
<!-- Common/Favorite terms section -->
<details class="favourite-terms" hidden>
- <summary class="title row btw">Your Go Tos:</summary>
- <ul class="favourite-list row btw"></ul>
+ <summary class="title row x-btw">Your Go Tos:</summary>
+ <ul class="favourite-list row x-btw"></ul>
</details>
<!-- Pagination info -->
@@ -130,7 +130,7 @@
{ <span>loading items</span> }
</p>
<!-- Terms list -->
- <ul class="items-container col start" role="listbox" aria-label="Available terms">
+ <ul class="items-container col top" role="listbox" aria-label="Available terms">
<!-- Terms will be populated here -->
</ul>
@@ -149,7 +149,7 @@
<!-- Create new term section -->
<details class="create-term" hidden>
- <summary class="row btw">Add New Term</summary>
+ <summary class="row x-btw">Add New Term</summary>
<div class="create-new-term-section">
<form class="create-term" data-nocache data-form-id="create-term" data-save="terms">
<div class="form-group">
@@ -229,7 +229,7 @@
?>
<div class="jvb-selector <?= esc_attr($this->name) ?>"
id="<?= esc_attr($this->id) ?>"<?= $hidden ?>>
- <div class="field-group-header row btw">
+ <div class="field-group-header row x-btw">
<label for="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete">
<?= ($this->config['icon']) ? jvbIcon($this->config['icon']) : '' ?>
<span><?= $this->config['label'] ?></span>
diff --git a/inc/helpers/crud.php b/inc/helpers/crud.php
index c5e3611..dc646c9 100644
--- a/inc/helpers/crud.php
+++ b/inc/helpers/crud.php
@@ -314,7 +314,7 @@
}
echo '<div class="container">';
- $nav = '<nav class="tabs row start" role="tablist">';
+ $nav = '<nav class="tabs row left" role="tablist">';
$i = 1;
foreach ($registrar->getSections() as $slug => $section) {
$nav .= '<button type="button" class="tab';
diff --git a/inc/helpers/media.php b/inc/helpers/media.php
index 82c6f3f..6381b82 100644
--- a/inc/helpers/media.php
+++ b/inc/helpers/media.php
@@ -25,7 +25,7 @@
<img src="" alt="" class="image">
<img src="" alt="" class="image-right">
<details>
- <summary class="row btw">DETAILS</summary>
+ <summary class="row x-btw">DETAILS</summary>
<div class="item-info"></div>
</details>
</div>
diff --git a/inc/helpers/renderFields.php b/inc/helpers/renderFields.php
index ccf021a..150af0e 100644
--- a/inc/helpers/renderFields.php
+++ b/inc/helpers/renderFields.php
@@ -319,7 +319,7 @@
$date = new Date('M j, Y', strtotime($review['date']));
}
- $out .= '<cite class="row btw">';
+ $out .= '<cite class="row x-btw">';
if ($review['rating'] !== 'none') {
$out .= jvbFormatStarRating($review['rating']);
}
@@ -449,7 +449,7 @@
</div>
</div>
<details>
- <summary class="row btw">
+ <summary class="row x-btw">
Extra Fields
</summary>
<div class="fields"></div>
diff --git a/inc/helpers/ui.php b/inc/helpers/ui.php
index 953ce1c..aed22d6 100644
--- a/inc/helpers/ui.php
+++ b/inc/helpers/ui.php
@@ -19,8 +19,8 @@
}
?>
- <aside id="queue" class="left col start btw main" aria-expanded="false" >
- <div class="m-actions row start nowrap">
+ <aside id="queue" class="left col top main" aria-expanded="false" >
+ <div class="m-actions row left nowrap">
<button class="refresh row" title="Check now">
<?= jvbIcon('arrows-clockwise', ['title'=> 'Check now']) ?>
<span class="countdown row indicator" title="Will refresh again...">5</span>
@@ -28,7 +28,7 @@
<div class="popup row"><span></span></div>
</div>
- <div class="header col start">
+ <div class="header col top">
<h2>Queue Status</h2>
<nav class="filters">
<?php
@@ -55,9 +55,9 @@
?>
</nav>
</div>
- <div class="qitems item-grid col a-start nowrap">
+ <div class="qitems item-grid col left nowrap">
</div>
- <div class="queue-actions row btw nowrap">
+ <div class="queue-actions row x-btw nowrap">
<button class="dismiss-all">Clear Completed</button>
<button class="retry-all">Retry Failed</button>
</div>
@@ -70,20 +70,20 @@
</button>
<template class="queueItem">
<div class="item">
- <div class="header row btw">
+ <div class="header row x-btw">
<span class="type"></span>
<span class="status row"><?= jvbIcon('arrows-clockwise') ?><span class="screen-reader-text"></span></span>
</div>
<?php jvbRenderProgressBar('',false,false) ?>
<div class="info">
<div class="details"></div>
- <div class="time row start">
+ <div class="time row left">
<?= jvbIcon('clock') ?>
<span class="started">Started: <time></time></span>
<span class="completed" hidden><span>Completed: </span><time></time></span>
</div>
</div>
- <div class="actions row end">
+ <div class="actions row right">
<button class="retry" data-action="retry"><span>Retry</span><?= jvbIcon('arrows-clockwise')?></button>
<button class="cancel" data-action="cancel"><span>Cancel</span><?= jvbIcon('x-square')?></button>
<button class="refresh" data-action="refresh" title="Refresh to see changes"><span>Refresh</span><?= jvbIcon('arrows-clockwise')?></button>
@@ -212,9 +212,9 @@
* Outputs the search bar (likely don't need anymore)
* @return string
*/
-function jvbSearch(string $placeholder = 'Search...', string $id = 'search'):string
+function jvbSearch(string $placeholder = 'Search...', string $id = 'search', string $label = '', string $buttonText = '', bool $buttonInside = false, bool $hideSearch = false):string
{
- return Form::search($placeholder, $id);
+ return Form::search($placeholder, $id, $label, $buttonText, $buttonInside, $hideSearch);
}
@@ -271,7 +271,7 @@
</aside>
<template class="notificationPopup">
<div class="toast" role="status" aria-live="polite">
- <div class="toast-content row btw">
+ <div class="toast-content row x-btw">
<p></p>
<button type="button" class="close-toast" aria-label="Close">
<?= jvbIcon('x') ?>
@@ -362,7 +362,7 @@
echo '';
return '';
}
- $header = '<nav class="tabs row start" role="tablist">';
+ $header = '<nav class="tabs row left" role="tablist">';
$content = '';
$i = 0;
@@ -434,7 +434,7 @@
<div class="bar">
<div class="fill"></div>
</div>
- <div class="row btw">
+ <div class="row x-btw">
%s
<div class="details">
%s
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 3a6643a..5a14350 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -2945,7 +2945,7 @@
?>
<form id="<?=$this->service_name?>" class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>"
data-service="<?php echo esc_attr($this->service_name); ?>">
- <div class="header row btw">
+ <div class="header row x-btw">
<h3><?php echo esc_html($this->title); ?></h3>
<div class="setup">
<?php if ($is_connected): ?>
@@ -3056,7 +3056,7 @@
}
?>
</div>
- <div class="actions row btw wrap">
+ <div class="actions row x-btw wrap">
<?php
foreach ($this->buttons as $action => $label) {
if (!$is_connected && $action !== 'save_credentials') {
diff --git a/inc/managers/DashboardManager.php b/inc/managers/DashboardManager.php
index eea1901..36eaff2 100644
--- a/inc/managers/DashboardManager.php
+++ b/inc/managers/DashboardManager.php
@@ -704,9 +704,9 @@
<?php
$menu = new Navigation('sidebar');
- $menuClasses = ['col', 'a-start', 'nowrap'];
+ $menuClasses = ['col', 'left', 'nowrap'];
$itemClasses = ['col'];
- $menu->addClass('col a-start')->hasToggle()->defaultMenuClasses($menuClasses);
+ $menu->addClass('col left')->hasToggle()->defaultMenuClasses($menuClasses);
$menu->defaultItemClasses($itemClasses);
$pages = $this->getUserAllowedPages()?:[];
//Dashboard
@@ -1005,7 +1005,7 @@
{
?>
<div class="approvals container">
- <nav class="tabs row start" role="tablist">
+ <nav class="tabs row left" role="tablist">
<button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true">
<?= jvbDashIcon('infinity')?>All
</button>
@@ -1077,7 +1077,7 @@
}
ob_start();
?>
- <nav class="tabs row start" role="tablist">
+ <nav class="tabs row left" role="tablist">
<?php
$i=1;
$content = Registrar::getRegistered('post');
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index 1d5c15a..c81010d 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -373,7 +373,7 @@
$config = Registrar::getInstance($slug);
$aOpen = '<a href="'.$directory['url'].'" title="See our list of '.$directory['title'].'">';
$aClose = '</a>';
- $cache .= '<li class="directory col start">
+ $cache .= '<li class="directory col left">
'. $aOpen.jvbIcon($config->getIcon() !== '' ? $config->getIcon() :'list-dashes').$directory['title'].$aClose;
if (!empty($directory['description'])) {
$cache .= '<div class="description">';
@@ -412,7 +412,7 @@
);
if ($current !== '' && array_key_exists($current, $this->directories())) {
$open = ($open) ? ' open' : '';
- $cache = '<details'.$open.'><summary class="row btw">Other '.$this->referAs(true).':</summary>'.
+ $cache = '<details'.$open.'><summary class="row x-btw">Other '.$this->referAs(true).':</summary>'.
str_replace('id="'.$current.'"', 'id="'.$current.'" class="current"', $cache)
.'</details>';
}
@@ -613,7 +613,7 @@
$children =$this->renderListChunk($taxonomy, $term->term_id);
$out .= '<li>';
if ($children !== '') {
- $out .= '<details class="term"><summary class="row btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.html_entity_decode($term->name).'">'.$term->name.'</a></summary>';
+ $out .= '<details class="term"><summary class="row x-btw"><a href="'.get_term_link($term->term_id, $term->taxonomy).'" title="See more '.html_entity_decode($term->name).'">'.$term->name.'</a></summary>';
$out .= $children;
$out .= '</details>';
} else {
@@ -668,7 +668,7 @@
$out = '<ul class="list-none">';
foreach ($list as $letter => $items) {
- $out .= '<li id="starts-with-'.$letter.'" class="row a-start btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
+ $out .= '<li id="starts-with-'.$letter.'" class="row top x-btw nowrap"><h3>'.strtoupper($letter).'</h3><ul>';
foreach ($items as $item) {
$extra = '';
if (!empty($item['extra'])) {
@@ -683,7 +683,7 @@
$item_html = apply_filters('jvb_directory_render_item', '', $item, $type, $extra);
if (empty($item_html)) {
- $item_html = '<li class="row btw">
+ $item_html = '<li class="row x-btw">
<a href="'.$item['url'].'" title="More about '.$item['name'].'">
'.$item['name'].'</a>'.$extra.'
</li>';
diff --git a/inc/managers/IconsManager.php b/inc/managers/IconsManager.php
index 3290923..f54120b 100644
--- a/inc/managers/IconsManager.php
+++ b/inc/managers/IconsManager.php
@@ -625,7 +625,7 @@
/**
* Get raw SVG content for CSS mask-image
*/
- protected function getRawSvg(string $name, ?string $style = null): ?string
+ public function getRawSvg(string $name, ?string $style = null): ?string
{
if (!$style) {
$style = $this->style;
@@ -678,6 +678,8 @@
$css .= 'details.all-filters summary::after,';
} elseif ($icon === 'link') {
$css .= 'input[type=url],';
+ } elseif ($icon === apply_filters('jvbSeparatorLogo', 'logo')) {
+ $css .= 'hr.logo::before,';
}
$css .= ".icon-{$icon}{$styleClass}{";
$css .= "--icon:url('data:image/svg+xml;base64,{$svg}');";
diff --git a/inc/managers/LoginManager.php b/inc/managers/LoginManager.php
index 970b5e6..68db551 100644
--- a/inc/managers/LoginManager.php
+++ b/inc/managers/LoginManager.php
@@ -549,7 +549,7 @@
{
$form = $this->action.'form';
?>
- <section class="login-box col btw">
+ <section class="login-box col y-btw">
<h1><?=$this->labels['title']?></h1>
<?= $this->labels['description'] ?>
@@ -568,7 +568,7 @@
}
?>
- <div class="options row btw">
+ <div class="options row x-btw">
<?php
switch ($this->action) {
case 'login': ?>
@@ -593,7 +593,7 @@
</div>
</section>
- <div class="navigation row btw">
+ <div class="navigation row x-btw">
<a href="<?= get_home_url() ?>">Home</a>
<?php
$privacy = get_privacy_policy_url();
@@ -635,7 +635,7 @@
%s
%s
%s
- <div class="row btw nowrap">
+ <div class="row x-btw nowrap">
<button type="submit" class="button button-primary button-large">%s</button>
%s
</div>
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index fe97fea..c521c15 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -1441,7 +1441,7 @@
<section class="copy">
<h4>Your Referral Link</h4>
- <div class="copy-group row btw nowrap">
+ <div class="copy-group row x-btw nowrap">
<code id="referral-link" class="copy-target"><?= esc_url($share_url) ?></code>
<button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link">
<?= jvbIcon('copy'); ?>
@@ -1452,7 +1452,7 @@
<h4>Your Code</h4>
- <div class="copy-group row btw nowrap">
+ <div class="copy-group row x-btw nowrap">
<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
<button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code">
<?= jvbIcon('copy'); ?>
@@ -1471,19 +1471,19 @@
</section>
<section class="stats-summary">
- <div class="row btw">
+ <div class="row x-btw">
<span class="stat-label">Total Referrals</span>
<span class="stat-value" data-stat="total">-</span>
</div>
- <div class="row btw">
+ <div class="row x-btw">
<span class="stat-label">Successful</span>
<span class="stat-value" data-stat="treated">-</span>
</div>
- <div class="row btw">
+ <div class="row x-btw">
<span class="stat-label">Pending</span>
<span class="stat-value" data-stat="pending">-</span>
</div>
- <div class="row btw highlight">
+ <div class="row x-btw highlight">
<span class="stat-label">Available Rewards</span>
<span class="stat-value" data-stat="rewards">$0.00</span>
</div>
@@ -2692,7 +2692,7 @@
<details open>
<summary>Your Code</summary>
<h3>Share Link</h3>
- <div class="row btw nowrap">
+ <div class="row x-btw nowrap">
<code id="referral-link" class="copy-target"><?= home_url('/?ref=' . $referral_code) ?></code>
<button type="button" class="copy-btn" data-target="referral-link" title="Copy referral link">
<?= jvbIcon('copy'); ?>
@@ -2700,7 +2700,7 @@
</button>
</div>
<h3>Share Code</h3>
- <div class="row btw nowrap">
+ <div class="row x-btw nowrap">
<code id="referral-code" class="copy-target"><?= esc_html($referral_code) ?></code>
<button type="button" class="copy-btn" data-target="referral-code" title="Copy referral code">
<?= jvbIcon('copy'); ?>
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 341debf..224569f 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -25,7 +25,6 @@
private function __construct()
{
$this->cache = Cache::for('breadcrumbs', MONTH_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user');
- $this->cache->flush();
if (JVB_TESTING) {
$this->cache->flush();
}
@@ -50,7 +49,9 @@
return [];
}
+
switch (true) {
+
case is_singular():
$key = get_queried_object_id();
break;
@@ -62,6 +63,10 @@
$obj = get_queried_object();
$key = $obj->taxonomy;
break;
+ case is_home():
+ $obj = get_queried_object();
+ $key = $obj->post_type;
+ break;
default:
$key = 'home';
break;
@@ -93,7 +98,7 @@
$obj = get_queried_object();
if (is_tax()) {
$crumbs = $this->addTaxonomyCrumbs($crumbs, $obj);
- } elseif (is_singular()) {
+ } elseif (is_singular() || is_home()) {
$crumbs = $this->addArchiveCrumbs($crumbs, $obj);
$hierarchy = $this->addSingularCrumbs($crumbs, $obj);
$crumbs = $crumbs + $hierarchy;
@@ -211,6 +216,9 @@
*/
private function addArchiveCrumbs(array $crumbs, object $obj): array
{
+ if (is_singular('page') || is_home()) {
+ return $crumbs;
+ }
$type = is_singular() ? $obj->post_type : $obj->name;
$name = jvbNoBase($type);
@@ -235,8 +243,9 @@
'url' => get_post_type_archive_link($type)
];
} else {
+ $postTypeObject = get_post_type_object($type);
$crumbs[] = [
- 'name' => $obj->label,
+ 'name' => $postTypeObject->label,
'url' => get_post_type_archive_link($type)
];
}
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index 4893787..55835f0 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -348,6 +348,10 @@
}
}
}
+ $isUpdate = $meta->get('is_update');
+ if (!(bool) $isUpdate) {
+ $meta->set('number', $index);
+ }
if ($lastKey === $index) {
$latestTimestamp = strtotime($post->post_date);
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index 99a128b..2cf3f05 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -463,7 +463,7 @@
if (!array_key_exists('class', $config)) {
$config['class'] = [];
}
- $config['class'][] ='row btw';
+ $config['class'][] ='row x-btw';
$checked = filter_var($value, FILTER_VALIDATE_BOOLEAN);
@@ -784,7 +784,7 @@
</div>
</div>
- <div class="selection-actions row btw" hidden>
+ <div class="selection-actions row x-btw" hidden>
<button type="button" data-action="add-to-group">
%sGroup
</button>
@@ -848,7 +848,7 @@
protected static function renderUploadItemActions(?int $attachmentId = null):string
{
return sprintf(
- '<div class="item-actions row btw">
+ '<div class="item-actions row x-btw">
<div class="btn">
<input type="radio" class="featured btn" name="featured" id="featured%s" hidden>
<label for="featured">
@@ -1207,7 +1207,7 @@
$containerId = sprintf('%s-%s-selector', $name, $config['subtype']);
$input = sprintf(
- '<div class="row btw">
+ '<div class="row x-btw">
<input type="hidden" name="%s" value="%s">
<label for="%s-autocomplete">%s<span>%s</span></label>',
esc_attr($name),
@@ -1224,7 +1224,7 @@
}
$plural = static::getPlural($config);
$input .= sprintf(
- '<div class="selected-items row start" role="region" aria-label="Selected %s"></div>',
+ '<div class="selected-items row left" role="region" aria-label="Selected %s"></div>',
$plural[1]??''
);
@@ -1498,7 +1498,7 @@
$config['data']['tag-format'] = esc_attr($tagFormat);
$input = sprintf(
- '<h3>%s</h3><div class="row start wrap">',
+ '<h3>%s</h3><div class="row left wrap">',
esc_html($config['label']??'')
);
@@ -1657,7 +1657,7 @@
%s
</button>
<details%s>
- <summary class="row btw repeater-row-header">
+ <summary class="row x-btw repeater-row-header">
<span class="drag-handle">%s</span>
<span class="row-number">#%s</span>
<span class="row-title">%s</span>
@@ -1729,8 +1729,8 @@
<div class="items-wrap">
<!-- Common/Favorite terms section -->
<details class="favourite-terms" hidden>
- <summary class="title row btw">Your Go Tos:</summary>
- <ul class="favourite-list row btw"></ul>
+ <summary class="title row x-btw">Your Go Tos:</summary>
+ <ul class="favourite-list row x-btw"></ul>
</details>
<!-- Pagination info -->
@@ -1748,7 +1748,7 @@
{ <span>loading items</span> }
</p>
<!-- Terms list -->
- <ul class="items-container col start" role="listbox" aria-label="Available terms">
+ <ul class="items-container col top" role="listbox" aria-label="Available terms">
<!-- Terms will be populated here -->
</ul>
@@ -1767,7 +1767,7 @@
<!-- Create new term section -->
<details class="create-term" hidden>
- <summary class="row btw">Add New Term</summary>
+ <summary class="row x-btw">Add New Term</summary>
<div class="create-new-term-section">
<form class="create-term" data-nocache data-form-id="create-term" data-save="terms">
<div class="form-group">
@@ -1826,20 +1826,33 @@
);
}
- public static function search(string $placeholder = 'Search...', string $id = 'search'):string
+ public static function search(string $placeholder = 'Search...', string $id = 'search', string $label = '', string $buttonText = '',bool $buttonInside = false, bool $hideSearch = false):string
{
$id = sanitize_title($id);
+ $label = empty($label) ? '' : sprintf(
+ '<h3>%s</h3>',
+ $label
+ );
+ $buttonText = empty($buttonText) ? '' : sprintf(
+ '<span>%s</span>',
+ $buttonText
+ );
+ $hideSearch = $hideSearch ? ' hidden' : '';
return sprintf(
- '<div class="search-container row start nowrap">
- <input type="search" id="%s" placeholder="%s">
+ '%s<div class="search-container row left nowrap%s">
+ <input type="search" id="%s" placeholder="%s"%s>
<button title="Clear Search" type="button" class="clear-search" aria-label="Clear search"
onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();">%s</button>
- <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s</button>
+ <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">%s%s</button>
</div>',
+ $label,
+ $buttonInside ? ' insideButton' : '',
$id,
$placeholder,
+ $hideSearch,
jvbIcon('x', ['title' => 'Clear Search']),
- jvbIcon('magnifying-glass')
+ jvbIcon('magnifying-glass'),
+ $buttonText
);
}
}
diff --git a/inc/templates.php b/inc/templates.php
index cf3505d..319618e 100644
--- a/inc/templates.php
+++ b/inc/templates.php
@@ -13,7 +13,7 @@
{
return '<template class="response">
<details class="response" open>
- <summary class="row btw">
+ <summary class="row x-btw">
<div class="header">
</div>
@@ -65,7 +65,7 @@
{
return '<template class="responses">
<details class="responses">
- <summary class="row btw">
+ <summary class="row x-btw">
Comments
</summary>
</details>
@@ -112,7 +112,7 @@
<h4>'.__('From:', 'jvb').'</h4>
<div class="source-info col">
<div class="source-day"></div>
- <div class="source-hours row start"></div>
+ <div class="source-hours row left"></div>
</div>
</div>
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index de191ce..307e0b9 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -578,7 +578,7 @@
protected function renderUploader(): void {
?>
<details open class="uploader">
- <summary class="row btw"><?= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?></summary>
+ <summary class="row x-btw"><?= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?></summary>
<form id="uploader" data-form-id="upload_new_<?=$this->dataType ?>">
<?php
echo jvbFormRestore();
@@ -624,7 +624,7 @@
return;
}
?>
- <details class="all-filters col start" data-ignore>
+ <details class="all-filters col top" data-ignore>
<summary>Filters</summary>
<?php
@@ -649,7 +649,7 @@
return;
}
?>
- <div class="search row start nowrap">
+ <div class="search row left nowrap">
<span class="label">Search:</span>
<?= jvbSearch() ?>
</div>
@@ -725,7 +725,7 @@
protected function renderOrderControls():void
{
?>
- <div class="radio-options order row btw w-full">
+ <div class="radio-options order row x-btw w-full">
<?php
$order = [
'orderby' => [
@@ -740,7 +740,7 @@
foreach ($order as $o => $option) {
?>
- <div class="row start">
+ <div class="row left">
<span class="label"><?= ucfirst($o)?>:</span>
<?php
$i = 0;
@@ -770,7 +770,7 @@
return;
}
?>
- <div class="filters row start">
+ <div class="filters row left">
<span class="label">Filters:</span>
<?php
foreach ($this->filters as $key => $config) {
@@ -928,7 +928,7 @@
ob_start();
?>
<details class="multi-select" title="Select columns" hidden>
- <summary class="row start nowrap">
+ <summary class="row left nowrap">
<?= jvbDashIcon('columns') ?>
<span class="labels">Toggle Columns</span>
</summary>
@@ -961,7 +961,7 @@
return;
}
?>
- <div class="bulk-controls row nowrap btw">
+ <div class="bulk-controls row nowrap x-btw">
<div class="bulk-select">
<input type="checkbox" id="select-all" class="select-all">
<label for="select-all" class="row"><span>Select All</span><span class="selected-count" hidden></span></label>
@@ -1132,7 +1132,7 @@
}
ob_start();
?>
- <div class="item-actions row btw abs">
+ <div class="item-actions row x-btw abs">
<?php
foreach ($this->itemActions as $action) {
$config = $this->defaultItemActions[$action];
@@ -1173,7 +1173,7 @@
<div class="item <?=esc_attr($this->dataType)?> row nowrap">
<?= $this->renderItemSelect()?>
<?=$this->renderImage() ?>
- <div class="col start w-full">
+ <div class="col top w-full">
<h3 data-field="post_title"></h3>
<p data-attr="date"></p>
<p data-field="price"></p>
@@ -1260,7 +1260,7 @@
<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
<?php
if (in_array('edit', $this->caps)) {
- echo $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '';
+ echo $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : '';
if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) {
$config['autocomplete'] = true;
}
@@ -1342,7 +1342,7 @@
$makeThisDetailed = (in_array($config['type'], $makeDetails));
?>
<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
- <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+ <?= $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : '' ?>
<?php
if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) {
$config['autocomplete'] = true;
@@ -1375,7 +1375,7 @@
$makeThisDetailed = (in_array($config['type'], $makeDetails));
?>
<td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
- <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+ <?= $makeThisDetailed ? '<details><summary class="row x-btw">See Value</summary>' : '' ?>
<?= Form::render($name, '', $config); ?>
<?= $makeThisDetailed ? '</details>' : '' ?>
</td>
@@ -1458,7 +1458,7 @@
protected function renderTableActions(): string {
ob_start();
?>
- <div class="table-actions row btw nowrap">
+ <div class="table-actions row x-btw nowrap">
<?php if (count(array_intersect(['create', 'edit'], $this->caps)) > 0) { ?>
<?= jvbRenderToggleTextField(
'vertical',
diff --git a/inc/ui/Checkout.php b/inc/ui/Checkout.php
index 974d09f..29be465 100644
--- a/inc/ui/Checkout.php
+++ b/inc/ui/Checkout.php
@@ -74,7 +74,7 @@
$form .= jvbRenderTabs($tabs, true);
- $form .= '<div class="cart-total row end">
+ $form .= '<div class="cart-total row right">
<p class="tax">Tax: <span></span></p>
<p class="total">GRAND TOTAL: <span></span></p>
</div>
@@ -239,7 +239,7 @@
<h3>Looks like we left things hanging</h3>
<p>We\'ve restored your cart from your last session below.</p>
<p>If you\'d rather start over, click the button below.</p>
- <div class="row btw">
+ <div class="row x-btw">
<button type="button" data-clear-cart>' . jvbIcon('trash') . 'Clear Cart</button>
<button type="button" data-dismiss>' . jvbIcon('x') . 'Dismiss</button>
</div>
diff --git a/inc/ui/Tabs.php b/inc/ui/Tabs.php
index 27ca6e7..909184a 100644
--- a/inc/ui/Tabs.php
+++ b/inc/ui/Tabs.php
@@ -48,7 +48,7 @@
return '';
}
- $header = '<nav class="tabs row start" role="tablist">';
+ $header = '<nav class="tabs row left" role="tablist">';
$content = '';
$i = 0;
diff --git a/inc/users/UserSettings.php b/inc/users/UserSettings.php
index 87a3659..246d3c1 100644
--- a/inc/users/UserSettings.php
+++ b/inc/users/UserSettings.php
@@ -171,7 +171,7 @@
<div class="notification-preferences">
<?php foreach ($favourites as $type => $items) : ?>
<details class="notification-group">
- <summary class="notification-group-header row btw">
+ <summary class="notification-group-header row x-btw">
<span class="type-label"><?=jvbIcon('heart', ['style' => 'fill'])?> <?= $notify[$type] ?></span>
<span class="type-count">( <?= count($items); ?> )</span>
</summary>
diff --git a/inc/utility/Image.php b/inc/utility/Image.php
index 83b95e7..0b0afc2 100644
--- a/inc/utility/Image.php
+++ b/inc/utility/Image.php
@@ -153,4 +153,10 @@
}
);
}
+
+ public static function getData(int $imgID):array
+ {
+ return (new Image)->getImageData($imgID);
+ }
+
}
diff --git a/jvb.php b/jvb.php
index 17669f9..d5da153 100644
--- a/jvb.php
+++ b/jvb.php
@@ -141,6 +141,13 @@
return IconsManager::for($source)->get($name, $options);
}
+function jvbFullIcon(string $name, array $options = []):string
+{
+ $source = $options['source'] ?? 'icons';
+ unset($options['source']);
+ return IconsManager::for($source)->getRawSvg($name, $options['style']??null);
+}
+
/**
* Get a CSS data URI for an icon
*
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..05da41a
--- /dev/null
+++ b/src/drawer-menu/render.php
@@ -0,0 +1,38 @@
+<?php
+
+use JVBase\managers\Cache;
+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 = Cache::for('drawer');
+
+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/faq/block.json b/src/faq/block.json
new file mode 100644
index 0000000..f88d33a
--- /dev/null
+++ b/src/faq/block.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/faq",
+ "title": "FAQ Block",
+ "category": "jvb",
+ "icon": "info",
+ "description": "Display FAQs organized by sections with customizable ordering",
+ "keywords": ["faq", "questions", "help"],
+ "version": "1.0.0",
+ "textdomain": "jvb",
+ "attributes": {
+ "sectionOrder": {
+ "type": "array",
+ "default": []
+ },
+ "showSectionTitles": {
+ "type": "boolean",
+ "default": true
+ },
+ "collapseByDefault": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "supports": {
+ "align": ["wide", "full"],
+ "html": false
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/faq/edit.js b/src/faq/edit.js
new file mode 100644
index 0000000..73e36e4
--- /dev/null
+++ b/src/faq/edit.js
@@ -0,0 +1,145 @@
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, ToggleControl, Notice, Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useState, useEffect } from '@wordpress/element';
+import './editor.scss';
+
+export default function Edit({ attributes, setAttributes }) {
+ const { sectionOrder, showSectionTitles, collapseByDefault } = attributes;
+ const [sections, setSections] = useState([]);
+
+ // Get sections from localized script
+ const allSections = window.jvbFaq?.sections || [];
+
+ // Initialize sections with proper ordering
+ useEffect(() => {
+ if (!allSections.length) return;
+
+ if (sectionOrder.length === 0) {
+ // First time - use default order
+ const orderedSections = allSections.map(section => ({
+ id: section.id,
+ name: section.name,
+ }));
+ setSections(orderedSections);
+ setAttributes({ sectionOrder: orderedSections.map(s => s.id) });
+ } else {
+ // Use saved order, add any new sections at the end
+ const orderedSections = [];
+ const existingIds = new Set(sectionOrder);
+
+ // Add sections in saved order
+ sectionOrder.forEach(id => {
+ const section = allSections.find(s => s.id === id);
+ if (section) {
+ orderedSections.push({ id: section.id, name: section.name });
+ }
+ });
+
+ // Add any new sections that weren't in the saved order
+ allSections.forEach(section => {
+ if (!existingIds.has(section.id)) {
+ orderedSections.push({ id: section.id, name: section.name });
+ }
+ });
+
+ setSections(orderedSections);
+ }
+ }, [allSections, sectionOrder]);
+
+ const moveSection = (index, direction) => {
+ const newSections = [...sections];
+ const newIndex = direction === 'up' ? index - 1 : index + 1;
+
+ if (newIndex < 0 || newIndex >= newSections.length) return;
+
+ // Swap sections
+ [newSections[index], newSections[newIndex]] = [newSections[newIndex], newSections[index]];
+
+ setSections(newSections);
+ setAttributes({ sectionOrder: newSections.map(s => s.id) });
+ };
+
+ const blockProps = useBlockProps({
+ className: 'faq-block-editor',
+ });
+
+ return (
+ <>
+ <InspectorControls>
+ <PanelBody title={__('FAQ Settings', 'jvb')} initialOpen={true}>
+ <ToggleControl
+ label={__('Show Section Titles', 'jvb')}
+ checked={showSectionTitles}
+ onChange={(value) => setAttributes({ showSectionTitles: value })}
+ help={__('Display section names as headings', 'jvb')}
+ />
+ <ToggleControl
+ label={__('Collapse by Default', 'jvb')}
+ checked={collapseByDefault}
+ onChange={(value) => setAttributes({ collapseByDefault: value })}
+ help={__('Questions start collapsed and expand on click', 'jvb')}
+ />
+ </PanelBody>
+
+ <PanelBody title={__('Section Order', 'jvb')} initialOpen={false}>
+ <p className="components-base-control__help">
+ {__('Use the arrow buttons to reorder sections', 'jvb')}
+ </p>
+ {sections.length > 0 ? (
+ <div className="faq-section-list">
+ {sections.map((section, index) => (
+ <div key={section.id} className="faq-section-item">
+ <div className="faq-section-controls">
+ <Button
+ icon="arrow-up-alt2"
+ label={__('Move up', 'jvb')}
+ disabled={index === 0}
+ onClick={() => moveSection(index, 'up')}
+ className="faq-section-button"
+ />
+ <Button
+ icon="arrow-down-alt2"
+ label={__('Move down', 'jvb')}
+ disabled={index === sections.length - 1}
+ onClick={() => moveSection(index, 'down')}
+ className="faq-section-button"
+ />
+ </div>
+ <span className="faq-section-name">{section.name}</span>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <Notice status="info" isDismissible={false}>
+ {__('No sections found. Create sections in the FAQ taxonomy.', 'jvb')}
+ </Notice>
+ )}
+ </PanelBody>
+ </InspectorControls>
+
+ <div {...blockProps}>
+ <div className="faq-block-preview">
+ <h3>{__('FAQ Block', 'jvb')}</h3>
+ <p>
+ {__('This block will display FAQs organized by sections.', 'jvb')}
+ </p>
+ {sections.length > 0 ? (
+ <div className="faq-sections-preview">
+ <strong>{__('Section Order:', 'jvb')}</strong>
+ <ol>
+ {sections.map((section) => (
+ <li key={section.id}>{section.name}</li>
+ ))}
+ </ol>
+ </div>
+ ) : (
+ <Notice status="warning" isDismissible={false}>
+ {__('No sections available. Create sections in the FAQ taxonomy.', 'jvb')}
+ </Notice>
+ )}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/src/faq/editor.scss b/src/faq/editor.scss
new file mode 100644
index 0000000..d9955a2
--- /dev/null
+++ b/src/faq/editor.scss
@@ -0,0 +1,99 @@
+/**
+ * FAQ Block - Editor Styles
+ */
+
+.faq-block-editor {
+ padding: 2rem;
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ background: #f9f9f9;
+
+ .faq-block-preview {
+ text-align: center;
+
+ h3 {
+ margin: 0 0 0.5rem;
+ font-size: 1.25rem;
+ font-weight: 600;
+ }
+
+ > p {
+ margin: 0 0 1.5rem;
+ color: #666;
+ }
+
+ .faq-sections-preview {
+ margin-top: 1.5rem;
+ text-align: left;
+ background: white;
+ padding: 1rem;
+ border-radius: 4px;
+
+ strong {
+ display: block;
+ margin-bottom: 0.5rem;
+ }
+
+ ol {
+ margin: 0;
+ padding-left: 1.5rem;
+
+ li {
+ margin: 0.25rem 0;
+ padding: 0.25rem 0;
+ }
+ }
+ }
+ }
+}
+
+// Inspector Controls
+.faq-section-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+.faq-section-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: background 150ms ease;
+
+ &:hover {
+ background: #f9f9f9;
+ }
+}
+
+.faq-section-controls {
+ display: flex;
+ gap: 0.25rem;
+ flex-shrink: 0;
+}
+
+.faq-section-button {
+ min-width: 30px !important;
+ padding: 4px !important;
+ height: 30px !important;
+
+ &:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ }
+}
+
+.faq-section-name {
+ flex: 1;
+ font-weight: 500;
+ padding-left: 0.5rem;
+}
+
+// Notice adjustments
+.components-panel__body .components-notice {
+ margin: 1rem 0;
+}
diff --git a/src/faq/index.js b/src/faq/index.js
new file mode 100644
index 0000000..2f2dd15
--- /dev/null
+++ b/src/faq/index.js
@@ -0,0 +1,11 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from './edit';
+import './style.scss';
+import './editor.scss';
+import metadata from './block.json';
+
+registerBlockType(metadata.name, {
+ edit: Edit,
+ // No save function - dynamic block rendered on server
+ save: () => null,
+});
diff --git a/src/faq/index.php b/src/faq/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/faq/index.php
diff --git a/src/faq/render.php b/src/faq/render.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/faq/render.php
diff --git a/src/faq/style.scss b/src/faq/style.scss
new file mode 100644
index 0000000..13564fa
--- /dev/null
+++ b/src/faq/style.scss
@@ -0,0 +1,72 @@
+nav#faq {
+ height: max-content;
+ display: block;
+ background-color: var(--base-100);
+ border-radius: var(--radius-outer);
+ padding: 1.5rem;
+ touch-action: auto;
+ ol {
+ list-style: decimal-leading-zero;
+ height: fit-content;
+ display: block;
+ counter-reset: faq;
+ li {
+ counter-increment: faq;
+ width: max-content;
+ &::before {
+ content: counter(faq);
+ display: block;
+ font-family: var(--heading);
+ font-weight: var(--fw-h-bold);
+ }
+ }
+ }
+ h2 {
+ left: 0;
+ font-size: var(--txt-large);
+ margin: .5rem 0 .5rem;
+ }
+ a {
+ padding: .5rem;
+ }
+}
+
+.faq-block {
+ padding-bottom: 3rem;
+ max-width: none;
+ width: 100%;
+ > * {
+ max-width: var(--wide);
+ margin: 1rem auto;
+ }
+ h2 {
+ margin: 5rem 0 1.5rem;
+ }
+ h3 {
+ margin: 0;
+ text-transform: none;
+ }
+ :target {
+ background-color: var(--base);
+ outline: none;
+
+ h2 {
+ background-color: var(--base);
+ padding: 1rem 1.5rem;
+ border-radius: var(--radius-outer);
+ }
+ }
+ details {
+ max-width: var(--content);
+ margin: 1rem auto;
+ padding: .75rem;
+ }
+ details + details {
+ margin-top: 3rem;
+ }
+ details .button {
+ height: fit-content;
+ display: flex;
+ margin-left: auto;
+ }
+}
diff --git a/src/faq/view.js b/src/faq/view.js
new file mode 100644
index 0000000..1de987d
--- /dev/null
+++ b/src/faq/view.js
@@ -0,0 +1,84 @@
+/**
+ * FAQ Block - Frontend Interactions
+ * Handles accordion functionality for FAQ items
+ */
+
+document.addEventListener('DOMContentLoaded', () => {
+ const faqBlocks = document.querySelectorAll('.faq-block');
+
+ faqBlocks.forEach((block) => {
+ const faqItems = block.querySelectorAll('.faq-item');
+
+ faqItems.forEach((item) => {
+ const button = item.querySelector('.faq-item__question');
+ const answer = item.querySelector('.faq-item__answer');
+
+ if (!button || !answer) return;
+
+ button.addEventListener('click', () => {
+ const isExpanded = button.getAttribute('aria-expanded') === 'true';
+
+ // Toggle this item
+ button.setAttribute('aria-expanded', !isExpanded);
+
+ if (isExpanded) {
+ // Collapse
+ answer.style.height = answer.scrollHeight + 'px';
+ // Force reflow
+ answer.offsetHeight;
+ answer.style.height = '0';
+
+ setTimeout(() => {
+ answer.style.display = 'none';
+ answer.style.height = '';
+ }, 300);
+
+ item.classList.remove('faq-item--expanded');
+ } else {
+ // Expand
+ answer.style.display = 'block';
+ answer.style.height = '0';
+ // Force reflow
+ answer.offsetHeight;
+ answer.style.height = answer.scrollHeight + 'px';
+
+ setTimeout(() => {
+ answer.style.height = 'auto';
+ }, 300);
+
+ item.classList.add('faq-item--expanded');
+ }
+ });
+
+ // Handle keyboard navigation
+ button.addEventListener('keydown', (e) => {
+ // Space or Enter triggers the button
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ button.click();
+ }
+ });
+ });
+ });
+
+ // Optional: Add URL hash navigation
+ // If URL has #faq-123, open that specific FAQ
+ if (window.location.hash) {
+ const hash = window.location.hash.substring(1);
+ const targetItem = document.querySelector(`[data-faq-id="${hash}"]`);
+
+ if (targetItem) {
+ const button = targetItem.querySelector('.faq-item__question');
+ const isExpanded = button.getAttribute('aria-expanded') === 'true';
+
+ if (!isExpanded) {
+ button.click();
+ }
+
+ // Scroll to item after a short delay
+ setTimeout(() => {
+ targetItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 350);
+ }
+ }
+});
diff --git a/src/feed/block.json b/src/feed/block.json
new file mode 100644
index 0000000..3fec37e
--- /dev/null
+++ b/src/feed/block.json
@@ -0,0 +1,57 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/feed",
+ "title": "Feed",
+ "category": "jvb",
+ "icon": "grid-view",
+ "description": "Displays a filterable feed of registered content types",
+ "keywords": [ "feed", "grid" ],
+ "version": "0.9.0",
+ "textdomain": "jvb",
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "attributes": {
+ "title": {
+ "type": "string",
+ "default": "Your Scene"
+ },
+ "inheritQuery": {
+ "type": "boolean",
+ "default": false
+ },
+ "contentTypes": {
+ "type": "array",
+ "default": ["tattoo", "artwork", "artist"],
+ "items": {
+ "type": "string"
+ }
+ },
+ "itemsPerPage": {
+ "type": "number",
+ "default": 36
+ },
+ "defaultOrder": {
+ "type": "string",
+ "default": "date_desc"
+ }
+ },
+ "selectors": {
+ "root": ".feed-block"
+ },
+ "styles": [
+ { "name": "default", "label": "Default", "isDefault": true },
+ { "name": "other", "label": "Other" }
+ ],
+ "example": {
+ "attributes": {
+ "message": "This is a notice!"
+ }
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/feed/edit.js b/src/feed/edit.js
new file mode 100644
index 0000000..2a5228d
--- /dev/null
+++ b/src/feed/edit.js
@@ -0,0 +1,256 @@
+/**
+ * Feed Block - Edit Component
+ * Fetches available feed types from /jvb/v1/feed/types
+ * Allows configuration of content types and inherit query setting
+ */
+
+import { useEffect, useState } from '@wordpress/element';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import {
+ PanelBody,
+ CheckboxControl,
+ ToggleControl,
+ Spinner,
+ Notice
+} from '@wordpress/components';
+import apiFetch from '@wordpress/api-fetch';
+import { __ } from '@wordpress/i18n';
+
+export default function Edit({ attributes, setAttributes }) {
+ const [feedTypes, setFeedTypes] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const blockProps = useBlockProps({
+ className: 'feed-block-editor'
+ });
+
+ /**
+ * Fetch available feed types on component mount
+ */
+ useEffect(() => {
+ apiFetch({
+ path: '/jvb/v1/feed/types',
+ headers: {
+ 'If-Modified-Since': localStorage.getItem('feed_types_modified'),
+ }
+ })
+ .then(types => {
+ setFeedTypes(types);
+ setLoading(false);
+
+ // Store Last-Modified for future requests
+ // (apiFetch doesn't expose response headers easily,
+ // but the server will handle 304s)
+
+ // Initialize contentTypes if not set and not inheriting
+ if (!attributes.contentTypes && !attributes.inheritQuery) {
+ const firstType = Object.keys(types)[0];
+ if (firstType) {
+ setAttributes({ contentTypes: [firstType] });
+ }
+ }
+ })
+ .catch(err => {
+ console.error('Error loading feed types:', err);
+ setError(err.message);
+ setLoading(false);
+ });
+ }, [attributes.inheritQuery]);
+
+ /**
+ * Toggle a content type in the selection
+ */
+ const toggleContentType = (slug, checked) => {
+ const currentTypes = attributes.contentTypes || [];
+
+ const newTypes = checked
+ ? [...currentTypes, slug]
+ : currentTypes.filter(t => t !== slug);
+
+ setAttributes({ contentTypes: newTypes });
+ };
+
+ /**
+ * Get friendly label for content type
+ */
+ const getTypeLabel = (slug, config) => {
+ return `${config.plural} (${config.type})`;
+ };
+
+ /**
+ * Group types by category for better UX
+ */
+ const groupedTypes = feedTypes ? {
+ content: Object.entries(feedTypes)
+ .filter(([_, config]) => config.type === 'content'),
+ taxonomy: Object.entries(feedTypes)
+ .filter(([_, config]) => config.type === 'taxonomy')
+ } : { content: [], taxonomy: [] };
+
+ return (
+ <div {...blockProps}>
+ <InspectorControls>
+ <PanelBody
+ title={__('Feed Settings', 'jvb')}
+ initialOpen={true}
+ >
+ <ToggleControl
+ label={__('Inherit from Page Context', 'jvb')}
+ help={
+ attributes.inheritQuery
+ ? __('Feed will adapt to the current page (profile, taxonomy, etc.)', 'jvb')
+ : __('Manually select content types to display', 'jvb')
+ }
+ checked={attributes.inheritQuery}
+ onChange={(value) => setAttributes({ inheritQuery: value })}
+ />
+
+ {!attributes.inheritQuery && (
+ <>
+ {loading && (
+ <div style={{ textAlign: 'center', padding: '20px' }}>
+ <Spinner />
+ <p>{__('Loading feed types...', 'jvb')}</p>
+ </div>
+ )}
+
+ {error && (
+ <Notice status="error" isDismissible={false}>
+ {__('Error loading feed types: ', 'jvb')} {error}
+ </Notice>
+ )}
+
+ {!loading && !error && feedTypes && (
+ <>
+ {groupedTypes.content.length > 0 && (
+ <>
+ <h4>{__('Content Types', 'jvb')}</h4>
+ {groupedTypes.content.map(([slug, config]) => (
+ <CheckboxControl
+ key={slug}
+ label={getTypeLabel(slug, config)}
+ checked={
+ attributes.contentTypes?.includes(slug) || false
+ }
+ onChange={(checked) =>
+ toggleContentType(slug, checked)
+ }
+ help={
+ config.taxonomies?.length > 0
+ ? `Filters: ${config.taxonomies.join(', ')}`
+ : null
+ }
+ />
+ ))}
+ </>
+ )}
+
+ {groupedTypes.taxonomy.length > 0 && (
+ <>
+ <h4 style={{ marginTop: '20px' }}>
+ {__('Content Taxonomies', 'jvb')}
+ </h4>
+ <p style={{ fontSize: '12px', color: '#757575' }}>
+ {__('These are collections that group other content', 'jvb')}
+ </p>
+ {groupedTypes.taxonomy.map(([slug, config]) => (
+ <CheckboxControl
+ key={slug}
+ label={getTypeLabel(slug, config)}
+ checked={
+ attributes.contentTypes?.includes(slug) || false
+ }
+ onChange={(checked) =>
+ toggleContentType(slug, checked)
+ }
+ help={
+ config.for_content?.length > 0
+ ? `Contains: ${config.for_content.join(', ')}`
+ : null
+ }
+ />
+ ))}
+ </>
+ )}
+
+ {!attributes.contentTypes?.length && (
+ <Notice status="warning" isDismissible={false}>
+ {__('Please select at least one content type', 'jvb')}
+ </Notice>
+ )}
+ </>
+ )}
+ </>
+ )}
+ </PanelBody>
+
+ <PanelBody
+ title={__('Display Settings', 'jvb')}
+ initialOpen={false}
+ >
+ <ToggleControl
+ label={__('Show Gallery View', 'jvb')}
+ help={__('Enable lightbox for images', 'jvb')}
+ checked={attributes.enableGallery || false}
+ onChange={(value) =>
+ setAttributes({ enableGallery: value })
+ }
+ />
+ </PanelBody>
+ </InspectorControls>
+
+ <div className="feed-block-placeholder">
+ <div className="feed-block-icon">
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none">
+ <rect x="3" y="3" width="7" height="7" fill="currentColor" opacity="0.3" />
+ <rect x="13" y="3" width="7" height="7" fill="currentColor" opacity="0.3" />
+ <rect x="3" y="13" width="7" height="7" fill="currentColor" opacity="0.3" />
+ <rect x="13" y="13" width="7" height="7" fill="currentColor" opacity="0.3" />
+ </svg>
+ </div>
+
+ <h3>{__('Feed Block', 'jvb')}</h3>
+
+ {attributes.inheritQuery ? (
+ <p className="feed-block-description">
+ {__('📍 Inheriting from page context', 'jvb')}
+ </p>
+ ) : (
+ <div className="feed-block-description">
+ {attributes.contentTypes?.length > 0 ? (
+ <>
+ <p><strong>{__('Showing:', 'jvb')}</strong></p>
+ <ul style={{
+ listStyle: 'none',
+ padding: '0',
+ margin: '8px 0'
+ }}>
+ {attributes.contentTypes.map(type => {
+ const config = feedTypes?.[type];
+ return (
+ <li key={type} style={{
+ padding: '4px 0',
+ color: '#2271b1'
+ }}>
+ ✓ {config?.plural || type}
+ </li>
+ );
+ })}
+ </ul>
+ </>
+ ) : (
+ <p style={{ color: '#d63638' }}>
+ {__('⚠️ No content types selected', 'jvb')}
+ </p>
+ )}
+ </div>
+ )}
+
+ <p className="feed-block-note">
+ {__('Feed will be displayed on the frontend', 'jvb')}
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/src/feed/editor.scss b/src/feed/editor.scss
new file mode 100644
index 0000000..8f0baab
--- /dev/null
+++ b/src/feed/editor.scss
@@ -0,0 +1,128 @@
+.feed-content-types {
+ margin-bottom: 16px;
+
+ .components-base-control__label {
+ margin-bottom: 8px;
+ font-weight: 500;
+ }
+
+ .components-checkbox-control {
+ margin-bottom: 8px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .components-checkbox-control__input-container {
+ margin-right: 8px;
+ }
+ }
+}
+.feed-block {
+ border: 1px solid #ddd;
+ padding: 20px;
+ background: white;
+
+ .feed-block-preview {
+ .filter-preview {
+ display: flex;
+ gap: 8px;
+ margin: 16px 0;
+ flex-wrap: wrap;
+
+ .content-type-badge {
+ background: #f0f0f0;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ }
+ }
+
+ .feed-grid-placeholder {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+ margin-top: 20px;
+
+ .grid-item-placeholder {
+ background: #f0f0f0;
+ aspect-ratio: 1;
+ border-radius: 4px;
+ }
+ }
+ }
+}
+
+.feed-content-types {
+ margin-bottom: 16px;
+
+ .components-base-control__label {
+ margin-bottom: 8px;
+ font-weight: 500;
+ }
+
+ .checkbox-list {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ max-height: 200px;
+ overflow-y: auto;
+ padding: 8px;
+ background: white;
+
+ .components-checkbox-control {
+ margin: 4px 0;
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .select-all-wrapper {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid #ddd;
+ }
+
+ .components-checkbox-control__input-container {
+ margin-right: 8px;
+ }
+}
+.feed-block {
+ border: 1px solid #ddd;
+ padding: 20px;
+ background: white;
+
+ .feed-block-preview {
+ .filter-preview {
+ display: flex;
+ gap: 8px;
+ margin: 16px 0;
+ flex-wrap: wrap;
+
+ .content-type-badge {
+ background: #f0f0f0;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ }
+ }
+
+ .feed-grid-placeholder {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 16px;
+ margin-top: 20px;
+
+ .grid-item-placeholder {
+ background: #f0f0f0;
+ aspect-ratio: 1;
+ border-radius: 4px;
+ }
+ }
+ }
+}
diff --git a/src/feed/index.js b/src/feed/index.js
new file mode 100644
index 0000000..c477d23
--- /dev/null
+++ b/src/feed/index.js
@@ -0,0 +1,39 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import save from './save';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+
+ /**
+ * @see ./save.js
+ */
+ save,
+} );
diff --git a/src/feed/index.php b/src/feed/index.php
new file mode 100644
index 0000000..6220032
--- /dev/null
+++ b/src/feed/index.php
@@ -0,0 +1,2 @@
+<?php
+// Silence is golden.
diff --git a/src/feed/render.php b/src/feed/render.php
new file mode 100644
index 0000000..0112ad1
--- /dev/null
+++ b/src/feed/render.php
@@ -0,0 +1,4 @@
+<?php
+if (!defined('ABSPATH')) {
+ exit; // Exit if accessed directly
+}
diff --git a/src/feed/save.js b/src/feed/save.js
new file mode 100644
index 0000000..8169594
--- /dev/null
+++ b/src/feed/save.js
@@ -0,0 +1,3 @@
+export default function save() {
+ return null; // Dynamic block rendered by PHP
+}
diff --git a/src/feed/style.scss b/src/feed/style.scss
new file mode 100644
index 0000000..3d4e777
--- /dev/null
+++ b/src/feed/style.scss
@@ -0,0 +1,956 @@
+//.feed-block {
+// max-width: var(--full);
+// margin: 0 auto;
+//
+// &:target {
+// scroll-snap-margin-top: 5rem;
+// scroll-margin-top: 5rem;
+// outline: 0;
+// border-radius: 0;
+// padding: 0;
+//
+// .feed-item {
+// outline: double var(--pink-0);
+// }
+// }
+//}
+//
+//.loading .feed-block {
+// opacity: .7;
+//}
+//
+//.label {
+// display: flex;
+// align-items: center;
+// gap: .25rem;
+// font-size: .9rem;
+// a:hover {
+// color: var(--pink-0);
+// }
+//}
+//
+///** Filters Form **/
+//.feed-filters {
+// margin: 2rem 0;
+// position: sticky;
+// top: 3rem;
+// z-index: 15;
+// background: rgba(var(--base-rgb),var(--op-6));
+// padding: .25rem 3rem;
+// details[open] summary {
+// background-color: var(--overlay);
+// }
+// summary {
+// justify-content: flex-start;
+//
+// > * {
+// order: 3;
+// }
+// .label {
+// order: 1;
+// }
+// .filter-label {
+// order: 2;
+// }
+// &::after {
+// order: 4;
+// }
+// #favourites + label {
+// margin-left: auto;
+// }
+// #favourites + label:hover,
+// #favourites:checked + label {
+// border-color: var(--pink-0);
+// background-color: var(--pink-0);
+// color: var(--white);
+//
+// }
+// #favourites:checked + label {
+// animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+// }
+// }
+//
+// details[open],
+// summary:hover {
+// background-color: rgba(var(--base-rgb),var(--op-6));
+// }
+//
+// &:has(#favourites) {
+// summary::after {
+// margin-left: 1rem;
+// }
+// }
+//}
+//summary > * {
+// order: 3;
+//}
+//summary .label {
+// order: 1;
+//}
+//.filter-label {
+// display: inline-block;
+// vertical-align: middle;
+// height: 1.3em;
+// order: 2;
+// margin: 0;
+// padding: 0;
+//
+// li {
+// list-style: none;
+// height: 0;
+// overflow: hidden;
+// &.active {
+// height: 100%;
+// }
+// }
+//}
+//.filter-group {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// flex-wrap: wrap;
+// label {
+// font-weight: 100;
+// }
+// &:has(.order-by) {
+// justify-content: space-between;
+// }
+// .order-by,
+// .order-direction {
+// display: flex;
+// flex-wrap: wrap;
+// gap: .5rem;
+// flex: 1;
+// justify-content: flex-start;
+// .label {
+// width: 100%;
+// }
+//
+// label:has(.label) {
+// padding: 0 .35rem;
+// gap: .5rem;
+// .label {
+// font-size: 1rem;
+// }
+// }
+// }
+//}
+//.filter-group >.label {
+// width: 100%;
+//}
+//
+///**
+//Feed Grid
+// */
+//.feed-grid {
+// display: grid;
+// grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+// gap: .5rem;
+// margin-bottom: 2rem;
+// padding: 0 4rem;
+// --delay: 0s;
+// --increase: .1s;
+//}
+//.feed-empty-state {
+// grid-column: 1/-1;
+// text-align: center;
+// padding: 2rem;
+// background: var(--base-100);
+// border-radius: var(--radius);
+// margin: 0 auto;
+// max-width: 600px;
+//}
+///**
+//Placeholders
+// */
+//.placeholder {
+// aspect-ratio: 1;
+// background: var(--base);
+// border: 1rem solid var(--base-50);
+// border-radius: 1rem;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+//
+// .icon {
+// --w: 50%;
+// color: var(--base-200);
+// animation: dance 2.5s ease-in-out infinite;
+//
+// }
+//}
+//
+///**
+//Feed Items
+// */
+//.feed-item {
+// position: relative;
+// border-radius: 0.5rem;
+// overflow: hidden;
+// background: var(--base-50);
+// box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+// opacity: 0;
+// transition: opacity var(--trans-base) var(--delay);
+// height: fit-content;
+// padding: 0;
+//
+// img {
+// opacity: .7;
+// filter: grayscale(.5) sepia(.3) blur(7px);
+// }
+// &[data-loaded=true] {
+// img {
+// opacity: 1;
+// filter: none;
+// }
+// }
+//
+// a {
+// &::before,
+// &::after {
+// display: none;
+// }
+// }
+// details a {
+// font-size: clamp(1rem, 0.9306rem + 0.2222vw, 1.125rem);
+// }
+//
+// &[data-loaded] {
+// opacity: 1;
+// + [data-loaded] {
+// --delay: var(--delay) + var(--increase);
+// }
+// }
+//
+// &.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;
+// text-transform: none;
+// .icon {
+// --w: 1.5em;
+// }
+// }
+//}
+///**
+//Load More Button
+// */
+//.load-more {
+// opacity: 1;
+// margin: 1rem auto;
+// width: 66.666%;
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// padding: .75rem 1.5rem;
+// background: var(--base);
+// color: var(--contrast);
+// border-radius: 4px;
+// font-size: var(--txt-medium);
+// transition: all var(--trans-base);
+// border: 2px solid transparent;
+// &[hidden] {
+// opacity: 0;
+// transition: all var(--trans-base);
+// }
+// &:hover {
+// background: var(--pink-50);
+// border-color: var(--contrast);
+// color: var(--white);
+// }
+//}
+///**
+//favourite button
+// */
+//button.favourite {
+// position: absolute;
+// top: .5rem;
+// right: .5rem;
+// z-index: 10;
+// background: rgba(var(--base-rgb),var(--op-4));
+// border-radius: 50%;
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw-subtle);
+// border: none;
+// width: 2rem;
+// height: 2rem;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// backdrop-filter: blur(5px);
+// transition: all var(--trans-base);
+//
+// &:hover {
+// transform: scale(1.1);
+// color: var(--pink-0);
+// background: var(--base);
+// box-shadow: 0 4px 8px rgba(0,0,0,0.15);
+// }
+//
+// &.favourited {
+// animation: pop 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+// }
+//}
+//
+//
+///** Images **/
+//.feed-image {
+// display: block;
+// aspect-ratio: 1;
+// overflow: hidden;
+// width: 100%;
+// height: 100%;
+//}
+//.feed-images {
+// width: 100%;
+// height: 100%;
+// &.multi {
+// display :grid;
+// grid-template-columns: repeat(3, 1fr);
+// grid-auto-rows: 1fr;
+// gap: 4px;
+//
+// > a {
+// width: 100%;
+// height: 100%;
+// aspect-ratio: 1;
+// }
+// .feed-image {
+// grid-row: span 2;
+// grid-column: span 2;
+// }
+// }
+// img {
+// width: 100%;
+// height: 100%;
+// object-fit: cover;
+// transition: transform var(--trans-t) var(--trans-fn);
+// }
+// a:hover img {
+// transform: scale(1.05);
+// }
+//}
+//
+//.feed-item {
+// &:nth-of-type(4n+2) .multi .feed-image {
+// grid-column: 2 / span 2;
+// grid-row: 1 / span 2;
+// }
+// &:nth-of-type(4n+3) .multi .feed-image {
+// grid-row: 2 / span 2;
+// grid-column: 1 / span 2;
+// }
+// &:nth-of-type(4n+4) .multi .feed-image {
+// grid-column: 2 / span 2;
+// grid-row: 2 / span 2;
+// }
+//}
+//
+///** Item Information **/
+//.item-info {
+// padding: .25rem;
+// border-left: 1px solid var(--base-200);
+// >div + div {
+// margin-top: .5em;
+// position: relative;
+//
+// &::before {
+// content: '';
+// display: block;
+// position: absolute;
+// top: -.3em;
+// left: -.25rem;
+// width: 66.6%;
+// border-bottom: 1px solid var(--base-200);
+// }
+// }
+// h3 {
+// margin: 0 0 .5em 0!important;
+// font-size: 1.1rem;
+// font-family: var(--body);
+// font-weight: var(--fw-b);
+// }
+// span {
+// text-transform: uppercase;
+// display: flex;
+// align-items: center;
+// }
+// .icon {
+// --w: 1.1em;
+// margin-right: .5em;
+// display: inline-block;
+// vertical-align: middle;
+// }
+//}
+//.item-list {
+// ul {
+// margin: 0;
+// padding: .5em 0;
+// display: flex;
+// flex-wrap: wrap;
+// gap: .5rem;
+// li {
+// list-style: none;
+// }
+// }
+// a {
+// background-color: var(--pink-0);
+// border: 1px solid transparent;
+// border-radius: 4px;
+// color: var(--light-0);
+// padding: .25em;
+// line-height: .8;
+// &:visited {
+// background-color: var(--pink-100);
+// color: var(--white);
+// }
+// &:visited:hover,
+// &:visited:focus,
+// &:hover,
+// &:focus {
+// background-color: transparent;
+// border-color: var(--contrast);
+// color: var(--contrast);
+// }
+// }
+//}
+///**
+//Loading
+// */
+//.loading {
+// opacity: .7;
+//}
+//
+//.loading-overlay {
+// position: fixed;
+// top: 0;
+// left: 0;
+// right: 0;
+// bottom: 0;
+// background-color: rgba(var(--base-rgb),var(--op-4));
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// transition: opacity 0.3s ease, visibility 0.3s ease;
+// z-index: 9999;
+// opacity: 0;
+// visibility: hidden;
+//}
+//.loading .loading-overlay {
+// opacity: 1;
+// visibility: visible;
+//
+// &::after {
+// content: '';
+// position: absolute;
+// z-index: -1;
+// inset: 0;
+// background: linear-gradient(
+// 90deg,
+// var(--shimmer)
+// );
+// animation: shimmer 3s ease-in-out infinite;
+// }
+//
+// .wrapper {
+// background-color: rgba(var(--base-rgb),var(--op-6));
+// padding: 2rem;
+// border-radius: var(--radius);
+// text-align: center;
+// max-width: 90%;
+// width: 400px;
+// height: 300px;
+// z-index: 5;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// position: relative;
+//
+// .spinner {
+// --h: 150px;
+// --w: calc(var(--h) * 2);
+// border-top: 5px solid var(--pink-0);
+// border-radius: 50%;
+// position: absolute;
+// width: var(--w);
+// height: var(--w);
+// top: calc(50% - var(--h));
+// left: calc(50% - var(--h));
+// opacity: .5;
+// z-index: 0;
+// animation: spin 1s var(--trans-t) infinite;
+// }
+// div.icon {
+// height: 50px;
+// width: 50px;
+//
+// .icon {
+// --w: 100%;
+// svg {
+// animation: dance 2s ease-in-out infinite;
+// transition: color 0.3s ease;
+// }
+// }
+// }
+// .status {
+// height: 200px;
+// width: 100%;
+// z-index: 5;
+// display: flex;
+// flex-direction: column;
+// align-items: center;
+//
+// h3 {
+// margin: 1.5rem 0 .25rem!important;
+// color: var(--contrast-200);
+// }
+// .message {
+// margin: 0;
+// max-width: 275px;
+// color: var(--contrast-100);
+// font-size: var(--txt-x-small);
+// animation: flicker 2s infinite;
+// }
+// }
+// }
+//}
+//
+//
+///* Animations */
+//@keyframes highlight {
+// 0%, 100% {
+// box-shadow: none;
+// }
+// 50% {
+// box-shadow: 0 0 0 4px var(--pink-0);
+// }
+//}
+//
+//@keyframes pop {
+// 0% { transform: scale(1); }
+// 50% { transform: scale(1.3); }
+// 75% { transform: scale(0.9); }
+// 100% { transform: scale(1); }
+//}
+//
+//@keyframes bubble {
+// 50% { box-shadow: 19px 0 0 3px, 38px 0 0 7px, 57px 0 0 3px }
+// 100% { box-shadow: 19px 0 0 0, 38px 0 0 3px, 57px 0 0 7px }
+//}
+//@keyframes highlight-pulse {
+// 0%, 100% { box-shadow: 0 0 0 4px #FF0080, 0 8px 16px rgba(0, 0, 0, 0.1); }
+// 50% { box-shadow: 0 0 0 8px #FF0080, 0 12px 24px rgba(0, 0, 0, 0.15); }
+//}
+//
+//@keyframes spin {
+// to { transform: rotate(360deg); }
+//}
+//
+//@keyframes shimmer {
+// 0% { transform: translateX(-100%); }
+// 50%, 100% { transform: translateX(100%); }
+//}
+//@keyframes dance {
+// 0%, 100% { transform: rotate(-5deg) scale(1);}
+// 50% { transform: rotate(5deg) scale(1.1); }
+//}
+//@keyframes flicker {
+// 0% { opacity: 0.6; }
+// 50% { opacity: 1; }
+// 100% { opacity: 0.6; }
+//}
+//
+//
+///**
+//Accessibility
+// */
+//
+///* Keyboard navigable feed items */
+//.feed-item[tabindex="0"] {
+// position: relative;
+//}
+//
+//.feed-item[tabindex="0"]::after {
+// content: '';
+// position: absolute;
+// top: 0;
+// left: 0;
+// right: 0;
+// bottom: 0;
+// pointer-events: none;
+// border: 2px solid transparent;
+// transition: border-color 0.2s ease;
+//}
+//
+//.feed-item[tabindex="0"]:focus::after {
+// border-color: #FF0080;
+//}
+//
+//
+//.feed-block .item {
+// summary {
+// a {
+// background-color: var(--action-0);
+// display: flex;
+// gap: .25rem;
+// flex-wrap: nowrap;
+// aspect-ratio: 1;
+// position: relative;
+// }
+// img {
+// width: 50%;
+// height: 100%;
+// object-fit: cover;
+// }
+//
+// }
+//}
+
+
+.feed-block {
+ grid-column: full;
+ .filters {
+ padding: 1rem 0;
+ max-width:var(--wide);
+ margin: 0 auto;
+
+ .remove-term.remove-term {
+ width: max-content;
+ height: max-content
+ }
+ }
+ .filter-group {
+ position: relative;
+ padding: 2rem 0;
+ .label {
+ position: absolute;
+ left: 0;
+ }
+ > .label {
+ top: 0;
+ }
+ [type=radio] {
+ position:absolute;
+ left: var(--offScreen);
+ }
+ button, label {
+ position: relative;
+ padding: .5rem;
+ height: max-content;
+ &:hover {
+ color: var(--action-contrast);
+ }
+ }
+ button:hover .label,
+ :checked + label .label,
+ label:hover .label {
+ opacity: 1;
+ visibility: visible;
+ }
+ &:has(label:hover) :checked + label .label,
+ button .label,
+ label .label {
+ --height: max-content;
+ opacity: 0;
+ visibility: hidden;
+ bottom: -2rem;
+ width: max-content;
+ white-space: nowrap;
+ font-weight: var(--fw-b);
+ }
+
+
+ }
+ h3 {
+ margin: 0 0 .25rem;
+ font-size: var(--medium);
+ }
+}
+/** PLACEHOLDERS **/
+.placeholder {
+ aspect-ratio: 1;
+ background: var(--base);
+ border: 1rem solid var(--base-50);
+ border-radius: 1rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ i.icon {
+ --w: 50%;
+ color: var(--base-200);
+ animation: dance 2.5s ease-in-out infinite;
+ }
+}
+
+.item-grid {
+ padding: 0 var(--chip);
+ max-width: 100%;
+}
+/** FEED ITEM **/
+.feed.item {
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ background: var(--base-50);
+ 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 {
+ object-fit: cover;
+ width: 100%;
+ height: 100%;
+ //opacity: .7;
+ //filter: grayscale(.5) sepia(.3);
+ &:hover {
+ opacity: .8;
+ }
+ }
+
+ &[data-timeline] {
+ .images {
+ aspect-ratio: 3/2;
+ padding: 0 0 1rem;
+ span {
+ width: 50%;
+ position: absolute;
+ background-color: var(--action-0);
+ color: var(--action-contrast);
+ padding: .25rem .5rem;
+ &:first-of-type {
+ bottom: 0;
+ right: 50%;
+ text-align: right;
+ }
+ &:last-of-type {
+ top: 0;
+ left: 50%;
+ }
+ }
+ > a {
+ position: relative;
+ display: flex;
+ flex-wrap: nowrap;
+ width: 100%;
+ height: 100%;
+ }
+ }
+ img {
+ width: 50%;
+ object-fit: cover;
+ height: 100%;
+ &:first-of-type {
+ border-right: 1px solid var(--action-0);
+ }
+ }
+ }
+
+
+ a {
+ &::before,
+ &::after {
+ display: none;
+ }
+ }
+
+ label {
+ font-weight: normal;
+ text-transform: none;
+ .icon {
+ --w: 1.5em;
+ }
+ }
+}
+
+
+.item-grid:has([data-timeline]) {
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+}
+
+.items-wrap [type=radio],
+.items-wrap [type=checkbox] {
+ position: absolute;
+ opacity: 0;
+ left: -200vw;
+}
+
+.items-wrap [type=radio] + label,
+.items-wrap [type=checkbox] + label {
+ position: relative;
+ cursor: pointer;
+}
+
+.items-wrap [type=radio] + label:hover,
+.items-wrap [type=checkbox] + label:hover {
+ color: var(--action-0);
+}
+
+.items-wrap [type=radio] + label::before,
+.items-wrap [type=checkbox] + label::before,
+.items-wrap [type=radio] + label::after,
+.items-wrap [type=checkbox] + label::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+}
+
+.items-wrap [type=radio] + label::after,
+.items-wrap [type=checkbox] + 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;
+}
+
+.items-wrap [type=radio] + label::before,
+.items-wrap [type=checkbox] + 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);
+}
+
+.items-wrap [type=radio]:hover + label::before,
+.items-wrap [type=checkbox]:hover + label::before {
+ border-color: var(--action-200);
+}
+
+.items-wrap [type=radio]:checked + label::before,
+.items-wrap [type=checkbox]:checked + label::before{
+ background-color: var(--action-0);
+ border-color: var(--action-100);
+}
+
+.items-wrap [type=radio]:checked + label::before {
+ border-radius: 50%;
+}
+
+.items-wrap [type=checkbox]: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;
+}
+
+.items-wrap :disabled + label {
+ cursor: not-allowed;
+ background-color: var(--base-50);
+ color: var(--base-200);
+ border-color: var(--base-200);
+}
+
+.items-wrap :disabled + label:hover {
+ background-color: var(--base-50);
+ color: var(--base-200);
+ border-color: var(--base-200);
+}
+
+.items-wrap :disabled + label::before {
+ border-color: var(--base-200);
+}
+
+#jvb-selector .items-wrap [type=radio] + label,
+#jvb-selector .items-wrap [type=checkbox] + label{
+ flex: 1;
+ padding-left: 2rem!important;
+ transform-origin: top center;
+ will-change: transform;
+}
+
+.feed-block + footer {
+ grid-column: full;
+ padding: 0!important;
+ margin: 0;
+ background-color: var(--base-50);
+ z-index: 0;
+ display: flex;
+ justify-content: flex-end;
+ button {
+ width: max-content;
+ margin-left: auto;
+ padding: .35rem .25rem;
+ --w: 1.3em!important;
+ flex-wrap: nowrap;
+ justify-content:flex-start;
+ transition: var(--trans-size);
+ font-size: var(--txt-x-small);
+ min-height: 0;
+ span {
+ display: none;
+ white-space: nowrap;
+ }
+ &:focus span,
+ &:hover span {
+ display: block;
+ }
+ }
+}
diff --git a/src/feed/view.js b/src/feed/view.js
new file mode 100644
index 0000000..87e79b9
--- /dev/null
+++ b/src/feed/view.js
@@ -0,0 +1,747 @@
+class FeedBlock {
+ constructor() {
+ this.container = document.querySelector('section.feed-block');
+ if(!this.container) return;
+
+ this.a11y = window.jvbA11y;
+ this.error = window.jvbError;
+ this.cache = new window.jvbCache('feed');
+ this.templates = window.jvbTemplates;
+
+ this.config = {
+ source: '',
+ context: '',
+ highlight: null,
+ gallery: false,
+ view: this.cache.get('feedView') || 'grid',
+ ... this.container.dataset
+ };
+
+ this.init();
+ }
+ init() {
+ this.initElements();
+ this.defineTemplates();
+ this.initListeners();
+ this.initFilters();
+
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initStore();
+ this.initTaxonomies();
+
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initStore();
+ this.initTaxonomies();
+
+ this.processCachedFilters();
+ this.processURLFilters();
+ this.updateFilterUI();
+ this.initGallery();
+ }, 100);
+ }
+ }
+
+ initElements() {
+ this.selectors = {
+ filterTrigger: '[data-filter]',
+ filters: {
+ actions: '.filter-actions .toggle-text',
+ container: '.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"]',
+ },
+ grid: '.item-grid',
+ selected: '.selected-items',
+ buttons: {
+ loadMore: 'button.load-more',
+ remove: '.remove-term',
+ clearFilters: 'button.clear-filters',
+ refresh: 'button[data-action="refresh"]'
+ }
+ };
+ this.ui = window.uiFromSelectors(this.selectors, this.container);
+ this.ui.buttons.refresh = document.querySelector(this.selectors.buttons.refresh);
+
+ //Add content and taxonomies
+ this.ui.content = this.ui.filters.container.querySelectorAll('[name="content"]');
+ if (this.ui.content.length === 0) this.ui.content = false;
+ this.ui.taxonomies = this.ui.filters.container.querySelectorAll('[data-taxonomy]');
+ if (this.ui.taxonomies.length === 0) this.ui.taxonomies = false;
+ this.ui.orderbyWrap = this.ui.filters.container.querySelector('[data-for-order]');
+ if (this.ui.orderbyWrap.length === 0) this.ui.orderbyWrap = false;
+ this.ui.order = this.ui.filters.container.querySelectorAll('[data-filter="order"]');
+ if (this.ui.order.length === 0) this.ui.order = false;
+ this.ui.orderby = this.ui.filters.container.querySelectorAll('[data-filter="orderby"]');
+ if (this.ui.orderby.length === 0) this.ui.orderby = false;
+
+ this.orderbyFilters = (this.ui.orderby)
+ ? Array.from(this.ui.orderby).map(o => o.value)
+ : [];
+
+ this.contentTypes = (this.ui.content)
+ ? Array.from(this.ui.content).map(c => c.value)
+ : [this.container.dataset.content];
+ this.taxonomies = (this.ui.taxonomies?.length > 0)
+ ? Array.from(this.ui.taxonomies).map(t => t.dataset.taxonomy)
+ : [];
+ }
+
+ initListeners() {
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ }
+
+ initFilters() {
+ this.allowedFilters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ let defaults = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1,
+ };
+ if (this.config.context) defaults.context = this.config.context;
+ if (this.config.source) defaults.source = this.config.source;
+
+ this.filters = defaults;
+ this.defaults = {...defaults};
+ }
+ updateFilterUI() {
+ if (this.ui.filters.container) {
+ //Get cached inputs
+ let groups = [
+ this.ui.content,
+ this.ui.orderby,
+ this.ui.order
+ ];
+
+ groups.forEach(group => {
+ if(group) {
+ for (let input of group) {
+ let [filter, value] = [input.dataset.filter, input.value];
+ if (!Object.hasOwn(this.store.filters, filter)) break;
+ let doit = this.store.filters[filter] === value;
+ if (doit) {
+ input.checked = doit;
+ break;
+ }
+ }
+ }
+ });
+
+ if (Object.hasOwn(this.store.filters, 'taxonomy')) {
+ for (let [taxonomy, terms] of Object.entries(this.store.filters.taxonomy)) {
+ terms.forEach(termId => {
+ termId = parseInt(termId);
+ const term = this.selector.store.get(termId);
+ if (term) {
+ this.createTermElement(termId);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ handlePopState(e) {
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ this.a11y.announce('Feed filters updated from browser history');
+ }
+ }
+ }
+
+ handleClick(e) {
+ if (window.targetCheck(e, this.selectors.buttons.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.selectors.buttons.clearFilters)) {
+ this.clearFilters();
+ }
+ let remove = window.targetCheck(e, this.selectors.buttons.remove);
+ if (remove) {
+ this.removeSelectedTerm(remove);
+ }
+
+ let refresh = window.targetCheck(e, this.selectors.buttons.refresh);
+ if (refresh) {
+ this.store.clearCache();
+ this.store.fetch();
+ }
+
+ let orderbyButton = window.targetCheck(e, '[data-filter="orderby"]');
+ if (orderbyButton && orderbyButton.value === 'random' && orderbyButton.checked) {
+ // Already selected random, just re-render to trigger new shuffle
+ this.renderItems();
+ }
+ }
+
+ nextPage() {
+ const nextPage = (this.store.filters.page || 1) + 1;
+ const maxPage = this.store.lastResponse?.pages || nextPage;
+ this.store.setFilters({ page: Math.min(nextPage, maxPage) });
+ }
+
+ handleChange(e) {
+ const target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (this.allowedFilters.includes(target.dataset.filter)) {
+ let filters = {};
+ filters[target.dataset.filter] = target.value;
+ this.resetFilters(filters);
+ }
+ switch (target.dataset.filter) {
+ case 'content':
+ this.updateContentFor(target.value);
+ break;
+ case 'orderby':
+ this.updateOrderOptions(target.value);
+ break;
+ }
+ }
+ }
+
+ clearFilters() {
+ this.taxFilters = {};
+ window.removeChildren(this.ui.selected);
+
+ this.taxonomies.forEach(tax => {
+ let fieldId = this.getFieldId(tax);
+ this.selector.selectedTerms.get(fieldId)?.clear();
+ });
+
+ this.store.setFilters({
+ ...this.defaults,
+ taxonomy: null
+ });
+
+ this.updateURL();
+ this.saveToCacheFilters();
+ }
+
+ resetFilters(filters) {
+ filters = {
+ ...this.store.filters,
+ page: 1,
+ ... filters
+ }
+ this.store.setFilters(filters);
+
+ this.updateURL();
+ this.saveToCacheFilters();
+ }
+
+ getFieldId(taxonomy) {
+ return this.selector.getFieldId(Array.from(this.ui.taxonomies).filter(tax => tax.dataset.taxonomy === taxonomy)[0]??null);
+ }
+ removeSelectedTerm(button) {
+ const termId = parseInt(button.dataset.id);
+ const taxonomy = button.dataset.taxonomy;
+
+ if (Object.hasOwn(this.taxFilters, taxonomy)){
+ this.taxFilters[taxonomy] = this.taxFilters[taxonomy]
+ .filter(id => id !== termId);
+ if (this.taxFilters[taxonomy].length === 0) {
+ delete this.taxFilters[taxonomy];
+ }
+ }
+ button.remove();
+
+ // Find the fieldId for this taxonomy
+ const field = this.getFieldId(taxonomy);
+ if (field) {
+ this.selector.activeField = field;
+ // Notify selector to remove from its selectedTerms
+ this.selector.removeSelected(termId, field);
+ }
+
+ this.resetFilters({
+ taxonomy: Object.keys(this.taxFilters).length > 0
+ ? this.taxFilters
+ : null
+ });
+ }
+
+ updateContentFor(content) {
+ let checkIt = [
+ this.ui.taxonomies,
+ this.ui.orderby
+ ];
+ checkIt.forEach(check => {
+ if (!check) return;
+ check.forEach(button => {
+ const forTypes = button.dataset.for?.split(',')??[];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(content);
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
+ });
+ });
+ }
+ updateOrderOptions(order) {
+ if (this.ui.orderbyWrap) {
+ let options = this.ui.orderbyWrap.dataset.forOrder.split(',')??[];
+ this.ui.orderbyWrap.hidden = !options.includes(order);
+ }
+ }
+
+ updateFilterControls() {
+ const isHidden = Object.keys(this.taxFilters).length === 0;
+ if (this.ui.buttons.clearFilters) {
+ this.ui.buttons.clearFilters.hidden = isHidden;
+ }
+ if (this.ui.filters.actions) {
+ this.ui.filters.actions.hidden = isHidden;
+ }
+ }
+
+ async initTaxonomies() {
+ this.taxFilters = {};
+ this.selector = window.jvbSelector;
+ // this.selector.scanExistingFields(this.ui.filters.container);
+ // this.taxonomies.map(tax => this.selector.batchFetch.add(tax));
+ // this.selector.batchFetchTaxonomies();
+ this.selector.subscribe((event, data) => {
+ switch (event) {
+ case 'selected-terms':
+
+ this.handleTaxonomyChange(data);
+ break;
+
+ }
+ });
+ }
+ handleTaxonomyChange(data) {
+ const {terms, taxonomy } = data;
+ if (terms.size === 0) return;
+ this.taxFilters[taxonomy] = Array.from(terms);
+ this.resetFilters({ taxonomy: this.taxFilters });
+
+ terms.forEach(t => {
+ this.createTermElement(t);
+ });
+ this.updateFilterControls();
+ }
+ getTaxonomyIcon(taxonomy) {
+ let iconButton = Array.from(this.ui.taxonomies)
+ .find(t => t.dataset.taxonomy === taxonomy);
+ return iconButton?.dataset.icon.trim() || 'tag';
+ }
+ createTermElement(termId) {
+ const term = this.selector.store.get(termId);
+ if (!term) return;
+ if (this.ui.selected.querySelector(`[data-id="${termId}"]`)) return;
+
+ term.icon = this.getTaxonomyIcon(term.taxonomy);
+ this.ui.selected.append(this.templates.create('feedTerm', term));
+ }
+
+ processCachedFilters() {
+ Object.keys(this.filters).forEach(filter => {
+ let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]) {
+ this.filters[filter] = cached;
+ }
+ });
+ }
+
+ processURLFilters() {
+ if (!this.isFirstPage()) return false;
+ const params = new URLSearchParams(window.location.search);
+ if (!params.toString()) return false;
+ let shouldUpdate = false;
+ this.allowedFilters.forEach(filter => {
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ shouldUpdate = true;
+ this.filters[filter] = value;
+ }
+ });
+
+ let hasTax = false;
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ hasTax = true;
+ shouldUpdate = true;
+ const taxonomy = key.replace('f_tax_','');
+ this.taxFilters[taxonomy] = value.split(',').map(Number);
+ }
+ });
+ if (shouldUpdate) {
+ if (hasTax) {
+ this.filters.taxonomy = this.taxFilters;
+ }
+ this.resetFilters(this.filters);
+ }
+ return true;
+ }
+
+ updateURL() {
+ const params = new URLSearchParams();
+ this.allowedFilters.forEach(key => {
+ if (Object.hasOwn(this.store.filters, key) && this.store.filters[key] !== this.defaults[key]) {
+ params.set(`f_${key}`, this.store.filters[key]);
+ }
+ });
+
+ for (let [tax, terms] of Object.entries(this.taxFilters)) {
+ if (terms.length > 0) {
+ params.set(`f_tax_${tax}`, terms.join(','));
+ }
+ }
+
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ const currentURL = window.location.pathname + window.location.search; // Change this line
+
+ if (newURL !== currentURL) {
+ window.history.pushState({filters:this.store.filters}, '', newURL);
+ }
+ }
+
+ saveToCacheFilters() {
+ Object.keys(this.store.filters).forEach(filter => {
+ const cacheKey = `${this.config.source}_${this.config.context}_${filter}`;
+
+ if (this.store.filters[filter] !== this.defaults[filter]) {
+ this.cache.set(cacheKey, this.store.filters[filter]);
+ } else {
+ this.cache.remove(cacheKey);
+ }
+ });
+
+ const taxCacheKey = `${this.config.source}_${this.config.context}_taxonomy`;
+ if (Object.keys(this.taxFilters).length > 0) {
+ this.cache.set(taxCacheKey, this.taxFilters);
+ } else {
+ this.cache.remove(taxCacheKey);
+ }
+ }
+
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse?.has_more) {
+ this.nextPage();
+ }
+ });
+ }
+ }
+ initStore() {
+ let extraOrderby = this.orderbyFilters.filter(v => !['date','modified','title','random'].includes(v));
+ let extraIndexes = [];
+ extraOrderby.forEach(orderby =>{
+ extraIndexes.push({name:orderby, keyPath: orderby});
+ });
+ const store = 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: 'date'},
+ { name: 'modified', keyPath: 'modified'},
+ { name: 'title', keyPath: 'title'},
+ ... extraIndexes
+ ],
+ filters: this.filters,
+ TTL: 6 * 60 * 60 * 1000, //6 hours
+ showLoading: true,
+ required: 'content',
+ }
+ );
+
+ this.store = store.feed;
+
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.renderItems(data.items);
+ this.ui.buttons.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse?.has_more) {
+ this.ui.buttons.loadMore.hidden = !this.store.lastResponse?.has_more??true;
+ }
+ break;
+ }
+ });
+ }
+
+ isFirstPage() {
+ return this.store.filters.page === 1;
+ }
+
+ renderItems(items = null) {
+ items = items??this.store.getFiltered();
+ if (this.isFirstPage()) {
+ window.removeChildren(this.ui.grid);
+ }
+ if (items.length === 0) {
+ this.showEmptyState();
+ this.a11y.announceItems(0, this.isFirstPage());
+ } else {
+ window.chunkIt(
+ items,
+ (item) => this.createItemElement(item),
+ (fragment) => {
+ this.removePlaceholders();
+ this.ui.grid.append(fragment);
+ if (this.config.gallery) this.gallery.buildGalleryItems('.item img');
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, !this.isFirstPage(), this.store.lastResponse?.has_more??false);
+ },
+ 5
+ ).then(()=>{});
+ }
+
+ this.updateFilterControls();
+ }
+
+ showEmptyState() {
+ window.removeChildren(this.ui.grid);
+ this.ui.grid.append(this.templates.create('emptyState'));
+ }
+
+ createItemElement(item) {
+ if (typeof item !== 'object') {
+ item = this.store.get(item);
+ if (!item) return;
+ }
+ return this.templates.create(`feedItem${window.uppercaseFirst(item.content)}`, item);
+ }
+ splitIDs(value) {
+ return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+ }
+
+ isImageField(item, value) {
+ if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+ return false;
+ }
+ let values = this.splitIDs(value);
+
+ return values.some(v =>
+ Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+ );
+ }
+ formatImageFields(element, value, item) {
+ let values = this.splitIDs(value); // Convert string to array first
+ if (values.length === 0) return;
+
+ if (values.length > 1) {
+ let image = element.querySelector('img');
+ if (!image) return;
+ values.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, values[0], item);
+ }
+ }
+ formatImageField(element, value, item) {
+ let imgData = item.images[value]??false;
+ if (!imgData) return;
+ [
+ element.src,
+ element.srcset,
+ element.alt
+ ] = [
+ imgData.tiny,
+ `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+ imgData['image-alt-text']
+ ]
+ }
+ isTaxonomyField(item, field) {
+ if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+ return false;
+ }
+
+ return Object.keys(item.taxonomies).includes(field);
+ }
+ formatTaxonomyField(element, item, field, value) {
+ if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+ let values = this.splitIDs(value);
+ if (values.length === 0) {
+ element.remove();
+ }
+ let listItem = element.querySelector('li');
+ for (let termID of values) {
+ let term = item.taxonomies[field][termID]??false;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
+
+ let title = window.decodeHTMLEntities(term.title);
+
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${title}`,
+ title
+ ];
+ element.append(termItem);
+ }
+ listItem.remove();
+ }
+ isTimeField(el) {
+ return el.tagName === 'TIME' || el.querySelector('time') !== null;
+ }
+ formatTimeField(element, value) {
+ if (element.tagName !== 'TIME') {
+ element = element.querySelector('time');
+ if (!element) return;
+ }
+ element.setAttribute('datetime', value);
+ element.textContent = window.formatTimeAgo(value, 'F Y');
+ }
+ formatField(element, value) {
+ element.textContent = window.decodeHTMLEntities(value);
+ }
+
+ addTimelineElements(item, template) {
+ let [
+ afterEl,
+ number,
+ started,
+ last
+ ] = [
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
+ ];
+
+ if (afterEl) {
+ afterEl.textContent = `After ${item.number - 1} Tx`;
+ }
+ if (number) {
+ number.textContent = item.number - 1;
+ }
+ if (started) {
+ this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+ }
+ if (last) {
+ this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+ }
+ }
+
+ removePlaceholders() {
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
+ }
+ }
+
+ defineTemplates() {
+ const T = this.templates;
+ const f = this;
+
+ T.define('feedTerm', {
+ refs: {
+ icon: '.icon',
+ span: 'span'
+ },
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ el.dataset.taxonomy = data.taxonomy;
+ if (refs.icon) refs.icon.className = `icon icon=${data.icon}`;
+ if (refs.span) refs.span.textContent = window.decodeHTMLEntities(data.name);
+ }
+ });
+ T.define('emptyState');
+
+ this.contentTypes.forEach(content => {
+ T.define(`feedItem${window.uppercaseFirst(content)}`, {
+ refs: {
+ link: 'a',
+ },
+ manyRefs: {
+ fields: '[data-field]',
+ },
+ setup({el, refs, manyRefs, data}) {
+ const isTimeline = Object.hasOwn(el.dataset, 'timeline');
+ if (manyRefs.fields) {
+ for (let field of manyRefs.fields) {
+ if (isTimeline && ['timeline','number'].includes(field.dataset.field)) continue;
+
+ const value = Object.hasOwn(data.fields, field.dataset.field)? data.fields[field.dataset.field] : false;
+ if (!value) {
+ field.remove();
+ continue;
+ }
+ if (f.isImageField(data, value)) {
+ f.formatImageField(field, value, data);
+ } else if (f.isTaxonomyField(data, field.dataset.field)) {
+ f.formatTaxonomyField(field, data, field.dataset.field, value);
+ } else if (f.isTimeField(field)) {
+ f.formatTimeField(field, value);
+ } else {
+ f.formatField(field, value);
+ }
+ }
+ if (refs.link && data.url !== '') {
+ refs.link.href = data.url;
+ refs.link.title = `View ${data.fields['post_title']??'Item'}`;
+ }
+ if (isTimeline ) f.addTimelineElements(data, el);
+ }
+ }
+ })
+ });
+ }
+
+ // addPlaceholders() {
+ // let total = this.contentTypes.length;
+ // const fragment = document.createDocumentFragment();
+ // for (let i = 0; i < 12; i++) {
+ // let template = window.getTemplate('placeholderTemplate');
+ //
+ // let rand = Math.floor(Math.random() * total);
+ // let icon;
+ // 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);
+ // }
+ // template.append(icon);
+ // fragment.append(template);
+ // }
+ // this.ui.grid.append(fragment);
+ // }
+}
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
+});
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
new file mode 100644
index 0000000..c65be60
--- /dev/null
+++ b/src/feed/viewOld.js
@@ -0,0 +1,770 @@
+class FeedBlockOld {
+ constructor() {
+ this.container = document.querySelector('section.feed-block');
+ if (!this.container) {
+ return;
+ }
+
+ this.a11y = window.jvbA11y;
+ this.cache = new window.jvbCache('feed');
+ this.error = window.jvbError;
+
+ this.config = {
+ source: '',
+ context: '',
+ highlight: null,
+ gallery: false,
+ view: this.cache.get('feedView') || 'grid',
+ ... this.container.dataset
+ };
+ this.initElements();
+ this.initFilters();
+
+
+ this.loadWhenAble();
+ }
+
+ loadWhenAble() {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, 100);
+ }
+ }
+
+ initElements() {
+ this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons
+ 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"]')??false;
+ this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+ 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,
+ ).map(content => content.dataset.taxonomy);
+ } else {
+ this.taxonomies = [];
+ }
+
+
+ }
+
+ async initTaxonomies() {
+ this.selector = window.jvbSelector;
+ const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
+
+ this.selector.isInitializing = true;
+ buttons.forEach((button) => {
+ const taxonomy = button.dataset.taxonomy;
+ this.currentTaxonomies.add(taxonomy);
+
+ this.selector.registerFilterButton(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax
+ });
+
+ // Add preload listeners
+ this.addTaxonomyPreloadListeners(button, taxonomy);
+ });
+
+ this.selector.isInitializing = false;
+
+ this.selector.subscribe((event, data) => {
+ if (event === 'selected-terms') this.handleTaxonomyChange(data);
+ });
+ }
+
+ addTaxonomyPreloadListeners(button, taxonomy) {
+ const preload = () => {
+ this.selector.preloadTaxonomy(taxonomy);
+ };
+
+ // Desktop hover
+ button.addEventListener('mouseenter', preload, { once: true });
+
+ // Touch/keyboard (fires before click)
+ button.addEventListener('pointerdown', preload, { once: true });
+
+ // Keyboard focus
+ button.addEventListener('focus', preload, { once: true });
+ }
+
+ handleTaxonomyChange(data) {
+ const { terms, taxonomy } = data;
+
+ // Update only the current taxonomy's terms
+ if (terms.size > 0) {
+ this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
+ } else {
+ // Remove taxonomy if no terms selected
+ delete this.taxonomyFilters[taxonomy];
+ }
+
+ // Build filters object with all taxonomies
+ let filters = {
+ page: 1
+ };
+
+ // Add taxonomy filters if any exist
+ if (Object.keys(this.taxonomyFilters).length > 0) {
+ filters.taxonomy = this.taxonomyFilters;
+ }
+
+ this.updateFilter(filters);
+ }
+
+ clearAllTaxonomies() {
+ this.taxonomyFilters = {};
+ window.removeChildren(this.ui.selectedTax);
+
+ this.updateFilter({
+ taxonomy: null,
+ page: 1
+ });
+ }
+
+ initFilters() {
+ //defaults
+ this.filters = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1
+ };
+ if (this.config.context) this.filters.context = this.config.context;
+ if (this.config.source) this.filters.source = this.config.source;
+
+ //check the cache
+ this.processCachedFilters();
+ //check url
+ this.processURLFilters();
+
+ // Set initial UI state
+ this.syncUIToFilters();
+ }
+ syncUIToFilters() {
+ 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);
+ }
+ nextPage() {
+ this.store.setFilter('page', this.store.filters.page++);
+ }
+
+ initStore() {
+ const store = 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: 6 * 60 * 60 * 1000,
+ showLoading: true,
+ required: 'content',
+ delayFetch: true
+ }
+ );
+ this.store = store.feed;
+
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.renderItems();
+ this.ui.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+ this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+ }
+ break;
+ }
+ });
+ }
+
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse) {
+ if (this.store.lastResponse['has_more']) {
+ this.nextPage();
+ }
+ }
+ });
+ }
+ }
+
+ processCachedFilters() {
+ Object.keys(this.filters).forEach(filter => {
+ let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]){
+ this.filters[filter] = cached;
+ }
+ });
+ }
+
+ processURLFilters() {
+ if (this.filters.page > 1) {
+ return false;
+ }
+ const params = new URLSearchParams(window.location.search);
+
+ if (!params.toString()) {
+ return false;
+ }
+ let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ filters.forEach(filter => {
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ this.filters[filter] = value;
+ let input = this.ui.filters[filter];
+ if (input) {
+ input.checked = true;
+ }
+ }
+ });
+
+ let hasTaxonomy = false;
+ // Load taxonomy filters from URL
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ hasTaxonomy = true;
+ const taxonomy = key.replace('f_tax_', '');
+ if (!this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = [];
+ }
+ this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
+ }
+ });
+ 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) {
+ if (button.dataset.fieldId) {
+ let field = this.selector.get(button.dataset.fieldId);
+ field.selectedTerms = new Set(ids);
+ this.selector.initFieldDisplay(button.dataset.fieldId);
+ } else {
+ this.selector.registerField(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax,
+ selectedItems: ids
+ });
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Update URL with current filters (for sharing/bookmarking)
+ */
+ updateURL() {
+ const params = new URLSearchParams();
+
+ // Add simple filters
+ ['content', 'order', 'orderby', 'match'].forEach(key => {
+ if (this.filters[key]) {
+ params.set(`f_${key}`, this.filters[key]);
+ }
+ });
+
+ // Add taxonomy filters
+ Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+ if (terms.length > 0) {
+ params.set(`f_tax_${taxonomy}`, terms.join(','));
+ }
+ });
+
+ // Update URL without reload
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ window.history.pushState({ filters: this.filters }, '', newURL);
+ }
+
+ renderItems() {
+ let items = this.store.getFiltered();
+ if (this.store.filters['page'] === 1) {
+ window.removeChildren(this.ui.grid);
+ }
+
+ if (items.length === 0) {
+ this.a11y.announceItems(0, this.store.filters['page'] > 0);
+ return;
+ }
+
+ const fragment = document.createDocumentFragment();
+ const batchSize = 10;
+
+ const processBatch = (startIndex) => {
+ const endIndex = Math.min(startIndex + batchSize, items.length);
+
+ for (let i = startIndex; i < endIndex; i++) {
+ const item = items[i];
+ const element = this.createItemElement(item);
+
+ fragment.appendChild(element);
+ }
+
+ if (endIndex < items.length) {
+ requestAnimationFrame(() => processBatch(endIndex));
+ } else {
+ this.removePlaceholders();
+ this.ui.grid.append(fragment);
+
+ if (this.config.gallery) {
+ this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+ }
+
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
+ }
+ };
+
+ if (items.length > 0) {
+ processBatch(0);
+ } else {
+ this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+ }
+
+ 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;
+ }
+ }
+
+ /**
+ *
+ * @param {object} item
+ */
+ createItemElement(item) {
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
+
+ const isTimeline = Object.hasOwn(template.dataset, 'timeline');
+
+ // Format fields using helpers
+ for (let [fieldName, value] of Object.entries(item.fields)) {
+ if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+ let el = template.querySelector(`[data-field="${fieldName}"]`);
+ if (!el) continue;
+
+ if (value === '') {
+ el.remove();
+ continue;
+ }
+
+ if (this.isImageField(item, value)) {
+ this.formatImageFields(el, value, item);
+ } else if (this.isTaxonomyField(item, fieldName)) {
+ this.formatTaxonomyField(el, item, fieldName, value);
+ } else if (this.isTimeField(el)) {
+ this.formatTimeField(el, value);
+ } else {
+ this.formatField(el, value);
+ }
+ }
+
+ // Handle link
+ let link = template.querySelector('a');
+ if (link && item.url !== '') {
+ [
+ link.href,
+ link.title
+ ] = [
+ item.url,
+ `View ${item.fields['post_title']??'Item'}`
+ ];
+ }
+
+ if (isTimeline) {
+ this.addTimelineElements(item, template);
+ }
+
+ return template;
+ }
+ splitIDs(value) {
+ return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+ }
+ isImageField(item, value) {
+ if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+ return false;
+ }
+ let values = this.splitIDs(value);
+
+ return values.some(v =>
+ Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+ );
+ }
+ formatImageFields(element, value, item) {
+ let values = this.splitIDs(value); // Convert string to array first
+ if (values.length === 0) return;
+
+ if (values.length > 1) {
+ let image = element.querySelector('img');
+ if (!image) return;
+ values.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, values[0], item);
+ }
+ }
+ formatImageField(element, value, item) {
+ let imgData = item.images[value]??false;
+ if (!imgData) return;
+ [
+ element.src,
+ element.srcset,
+ element.alt
+ ] = [
+ imgData.tiny,
+ `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+ imgData['image-alt-text']
+ ]
+ }
+ isTaxonomyField(item, field) {
+ if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+ return false;
+ }
+
+ return Object.keys(item.taxonomies).includes(field);
+ }
+ formatTaxonomyField(element, item, field, value) {
+ if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+ let values = this.splitIDs(value);
+ if (values.length === 0) {
+ element.remove();
+ }
+ let listItem = element.querySelector('li');
+ for (let termID of values) {
+ let term = item.taxonomies[field][termID]??false;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
+
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${term.title}`,
+ term.title
+ ];
+ element.append(termItem);
+ }
+ listItem.remove();
+ }
+ isTimeField(el) {
+ return el.tagName === 'TIME' || el.querySelector('time') !== null;
+ }
+ formatTimeField(element, value) {
+ if (element.tagName !== 'TIME') {
+ element = element.querySelector('time');
+ if (!element) return;
+ }
+ element.setAttribute('datetime', value);
+ element.textContent = window.formatTimeAgo(value, 'F Y');
+ }
+ formatField(element, value) {
+ element.textContent = value;
+ }
+
+ addTimelineElements(item, template) {
+ let [
+ afterEl,
+ number,
+ started,
+ last
+ ] = [
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
+ ];
+
+ if (afterEl) {
+ afterEl.textContent = `After ${item.fields.number} Tx`;
+ }
+ if (number) {
+ number.textContent = item.fields.number;
+ }
+ if (started) {
+ this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+ }
+ if (last) {
+ this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+ }
+ }
+
+ removePlaceholders() {
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
+ }
+ }
+
+
+ addPlaceholders() {
+ let total = this.contentTypes.length;
+ const fragment = document.createDocumentFragment();
+ for (let i = 0; i < 12; i++) {
+ let template = window.getTemplate('placeholderTemplate');
+
+ let rand = Math.floor(Math.random() * total);
+ let icon;
+ 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);
+ }
+ template.append(icon);
+ fragment.append(template);
+ }
+ this.ui.grid.append(fragment);
+ }
+
+
+
+ /**
+ *
+ * @param {object} filters {name: value}
+ */
+ updateFilter(filters) {
+ //double check filters are what we're expecting
+ let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
+
+ filters = Object.keys(filters)
+ .filter(key => allowed.includes(key))
+ .reduce((obj, key) => {
+ obj[key] = filters[key];
+ return obj;
+ }, {});
+
+ if (window.getDifferences.map(this.filters, filters)) {
+ this.filters = { ...this.filters, ...filters }; // Merge instead of replace
+ this.updateURL();
+ this.store.setFilters(filters);
+ }
+ }
+ /**
+ * Update visible filters based on selected content type
+ */
+ updateContentFor(contentType) {
+ // Update taxonomy filter visibility
+ const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+ taxonomyButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+ });
+
+ // Update ordering options
+ const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+ orderButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ if (forTypes.length > 0) {
+ button.hidden = !forTypes.includes(contentType);
+ // Uncheck if hiding
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
+ }
+ });
+
+ // Update order direction visibility based on selected orderby
+ const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+ this.updateOrderDirectionVisibility(orderBy?.value);
+ }
+
+ /**
+ * Show/hide order direction based on orderby selection
+ */
+ updateOrderDirectionVisibility(orderBy) {
+ const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+ if (orderDirection) {
+ const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+ orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+ }
+ }
+ /*********************************************************************
+ LISTENERS
+ *********************************************************************/
+ 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;
+ if ('IntersectionObserver' in window) {
+ this.imageObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ this.loadImage(entry.target);
+ this.imageObserver.unobserve(entry.target);
+ });
+ }, {
+ rootMargin: '100px',
+ threshold: .1
+ });
+ }
+
+ if ('ResizeObserver' in window) {
+ this.resizeObserver = new ResizeObserver(() => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ } else {
+ 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) {
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ this.a11y.announce('Feed filters updated from browser history');
+ }
+ }
+ }
+
+ handleClick(e) {
+ if (window.targetCheck(e, this.elements.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.elements.clearFilter)) {
+ this.clearAllTaxonomies();
+ } else if (window.targetCheck(e, '.remove-item')) {
+ this.handleRemoveSelectedTerm(e);
+ }
+ }
+
+ handleRemoveSelectedTerm(e) {
+ const selectedItem = e.target.closest('.selected-item');
+ if (!selectedItem) return;
+
+ const termId = parseInt(selectedItem.dataset.id);
+ const taxonomy = selectedItem.dataset.taxonomy;
+
+ // Remove from filters
+ if (this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+ .filter(id => id !== termId);
+
+ if (this.taxonomyFilters[taxonomy].length === 0) {
+ delete this.taxonomyFilters[taxonomy];
+ }
+ }
+
+ // Remove from UI
+ selectedItem.remove();
+
+ // Update filters
+ this.updateFilter({
+ taxonomy: Object.keys(this.taxonomyFilters).length > 0
+ ? this.taxonomyFilters
+ : null,
+ page: 1
+ });
+ }
+
+ handleChange(e) {
+ let target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (target.dataset.filter === 'content') {
+ this.updateContentFor(target.value);
+ this.updateFilter({ content: target.value, page: 1 });
+ } else if (target.dataset.filter === 'orderby') {
+ this.updateOrderDirectionVisibility(target.value);
+ this.updateFilter({ orderby: target.value, page: 1 });
+ } else if (target.dataset.filter === 'order') {
+ this.updateFilter({ order: target.value, page: 1 });
+ } else if (target.dataset.filter === 'match') {
+ this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+ } else if (target.dataset.filter === 'favourites') {
+ this.updateFilter({ favourites: target.checked, page: 1 });
+ }
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
+});
diff --git a/src/fields/block.json b/src/fields/block.json
new file mode 100644
index 0000000..085ccdf
--- /dev/null
+++ b/src/fields/block.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/fields",
+ "title": "JakeVan Fields",
+ "category": "jvb",
+ "icon": "ellipses",
+ "description": "Access data from your custom fields",
+ "keywords": [ "field", "custom", "jake" ],
+ "version": "0.9.0",
+ "textdomain": "jvb",
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "selectors": {
+ "root": ".jvb-f"
+ },
+ "styles": [],
+ "render": "file:./render.php",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/fields/edit.js b/src/fields/edit.js
new file mode 100644
index 0000000..6741773
--- /dev/null
+++ b/src/fields/edit.js
@@ -0,0 +1,29 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { SelectControl, ToggleControl, PanelBody } from '@wordpress/components';
+
+/**
+ * Styles
+ */
+import './editor.scss';
+
+/**
+ * Edit function for Summary Block
+ */
+export default function Edit({ attributes, setAttributes }) {
+ const blockProps = useBlockProps();
+
+ return (
+ <div {...blockProps}>
+ <div className="jvb-summary-preview">
+ <h3>{__('Summary', 'jvb')}</h3>
+ <p className="jvb-list-preview-note">
+ {__('This will inherit the current query to build the information from our custom meta on the front end.', 'jvb')}
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/src/fields/editor.scss b/src/fields/editor.scss
new file mode 100644
index 0000000..bdf5776
--- /dev/null
+++ b/src/fields/editor.scss
@@ -0,0 +1,20 @@
+/**
+ * Directory List Block Editor Styles
+ */
+.jvb-summary-preview {
+ padding: 20px;
+ background-color: #f8f9fa;
+ border: 1px solid #e2e4e7;
+ border-radius: 4px;
+
+ h3 {
+ margin-top: 0;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ff0080;
+ }
+ &-note {
+ font-style: italic;
+ color: #555d66;
+ margin-bottom: 0;
+ }
+}
diff --git a/src/fields/index.js b/src/fields/index.js
new file mode 100644
index 0000000..c477d23
--- /dev/null
+++ b/src/fields/index.js
@@ -0,0 +1,39 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import save from './save';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+
+ /**
+ * @see ./save.js
+ */
+ save,
+} );
diff --git a/src/fields/index.php b/src/fields/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/fields/index.php
diff --git a/src/fields/render.php b/src/fields/render.php
new file mode 100644
index 0000000..b5645eb
--- /dev/null
+++ b/src/fields/render.php
@@ -0,0 +1,320 @@
+<?php
+
+use JVBase\managers\Cache;
+use JVBase\meta\Meta;
+use JVBase\meta\Render;
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+ exit; // Exit if accessed directly
+}
+/**
+ * Summary Block Render
+ *
+ * @package Edmonton_Ink
+ */
+
+function jvbRenderSummaryBlock(array $attributes):string
+{
+
+ // Buffer output
+ if (is_tax()) {
+ switch (get_queried_object()->taxonomy) {
+ case BASE.'shop':
+ return jvbRenderShopSummary();
+ default:
+ return jvbRenderTermSummary();
+ }
+ } elseif (is_singular()) {
+ return jvbRenderArtistSummary();
+ }
+ return '';
+}
+
+function jvbRenderArtistSummary():string
+{
+ $current = get_queried_object();
+ $cache = Cache::for('artistSummary', WEEK_IN_SECONDS);
+ $key = $current->ID;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+ $meta = Meta::forPost($current->ID);
+ $artist = jvbContentFromUser((int)$current->post_author);
+
+ $registrar = Registrar::getInstance($current->post_type);
+ $sections = [];
+ if ($registrar) {
+ $sections = $registrar->getSections();
+ }
+
+
+
+
+// $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
+ ?>
+ <nav id="artist" class="on-this-page index">
+ <label>Jump to:
+ <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
+ <?= jvbIcon('plus-square')?>
+ </button>
+ </label>
+ <ul>
+ <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li>
+ <li><a href="#about">About</a></li>
+ <li><a href="#styles">Styles</a></li>
+ <li><a href="#contact">Contact</a></li>
+ <li><a href="#work">Work</a></li>
+ </ul>
+ </nav>
+ <header id="top">
+ <h1><small><?=(!empty($artist['city'])) ? $artist['city']['name'] :'Edmonton'?>'s Best <?= (!empty($artist['type'])) ?
+ $artist['type']['name']:'Tattoo Artists'?>:
+ </small><?=$artist['display_name']?></h1>
+ <div>
+ <?php if (!empty($artist['shop'])) : ?>
+ <ul class="term-list shop">
+ <li>
+ <a href="<?=$artist['shop']['url']?>" title="Learn more about <?=$artist['shop']['name']?>">
+ <?= strtolower($artist['shop']['name'])?>
+ </a>
+ </li>
+ </ul>
+ <?php endif; ?>
+ <?php if (!empty($artist['city'])): ?>
+ <ul class="term-list city">
+ <li>
+ <a href="<?=$artist['city']['url']?>" title="See who else is rocking out of <?=$artist['city']['name']?>">
+ <?= strtolower($artist['city']['name'])?>
+ </a>
+ </li>
+ </ul>
+ <?php endif; ?>
+ <?php $styles = $meta->get('top_styles');
+ if (!empty($styles)) {
+ ?>
+ <ul class="term-list style">
+ <?php
+ foreach ($styles as $style) {
+ $term = get_term((int)$style, BASE.'style');
+ if ($term && !is_wp_error($term)) {
+ $link = get_term_link((int)$style, BASE.'style');
+ ?>
+ <li>
+ <a href="<?=$link?>" title="Learn more about <?=html_entity_decode($term->name)?>">
+ <?=strtolower(html_entity_decode($term->name))?>
+ </a>
+ </li>
+ <?php
+ }
+ }
+ ?>
+ </ul>
+ <?php
+ }
+ ?>
+ </div>
+ </header>
+ <section>
+ <details class="bio-info">
+ <summary class="row x-btw">
+ <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2>
+ </summary>
+ <div class="columns stack-small">
+ <div class="column">
+ <?= Render::renderFrom($meta, 'image_portrait'); ?>
+ </div>
+ <div class="column">
+ <?= Render::renderFrom($meta, 'short_bio'); ?>
+ </div>
+ </div>
+ <div id="styles">
+ <h3>Works In</h3>
+ <?= jvbGetTheTerms('style', $current->ID) ?>
+ </div>
+ <div class="contact">
+ <h3>Contact:</h3>
+ <?php
+ echo jvbRenderContactInfo($current->ID, $meta);
+ echo jvbRenderLinks($current->ID, $meta);
+ ?>
+ </div>
+
+ <div id="about">
+ <?= Render::renderFrom($meta, 'bio')?>
+ </div>
+ </details>
+ </section>
+ <section id="contact" class="">
+ <h2>Contact <?=$artist['name']?></h2>
+ <?php
+ echo jvbRenderContactInfo($current->ID, 'post');
+ echo jvbRenderLinks($current->ID, 'post');
+ ?>
+ </section>
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
+
+function jvbRenderShopSummary()
+{
+ $current = get_queried_object();
+
+ $cache = Cache::for('shop_bio', WEEK_IN_SECONDS)->connect('taxonomy');
+ $key = $current->term_id;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+
+ $meta = Meta::forTerm($current->term_id);
+ $fields = $meta->getAll(['average_rating', 'established', 'bio','location','hours','specialties','awards','reviews']);
+ ?>
+ <nav id="shop" class="on-this-page index">
+ <label>Jump to:
+ <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
+ <?= jvbIcon('plus-square')?>
+ </button>
+ </label>
+ <ul>
+ <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php
+ if ($fields['rating'] !== 'none') {
+ ?>
+ <li><a href="#rating">Rating</a></li>
+ <?php
+ } elseif ($fields['opened'] !== '') {
+ ?>
+ <li><a href="#opened">Opened</a></li>
+ <?php
+ } elseif ($fields['location'] !== '') {
+ ?>
+ <li><a href="#location">Location</a></li>
+ <?php
+ } elseif ($fields['about'] !== '') {
+ ?>
+ <li><a href="#about">About</a></li>
+ <?php
+ } elseif ($fields['hours'] !== '') {
+ ?>
+ <li><a href="#hours">Hours</a></li>
+ <?php
+ } elseif ($fields['specialties'] !== '') {
+ ?>
+ <li><a href="#specialties">Specialties</a></li>
+ <?php
+ } elseif ($fields['awards'] !== '') {
+ ?>
+ <li><a href="#awards">Awards</a></li>
+ <?php
+ } elseif ($fields['reviews'] !== '') {
+ ?>
+ <li><a href="#reviews">Reviews</a></li>
+ <?php
+ }
+ ?>
+ <li><a href="#contact">Contact</a></li>
+ <li><a href="#artists">Artists</a></li>
+ </ul>
+ </nav>
+ <header id="top">
+ <div class="columns stack-small">
+ <div class="column">
+ <?=jvbFormatImage($meta->get('image'))?>
+ </div>
+ <div class="column">
+ <h1>
+ <small><?= (get_term((int)$meta->get('city'), BASE.'city')) ?
+ get_term((int)$meta->get('city'), BASE.'city')->name :
+ 'Edmonton'?>'s Best Tattoo Shops</small>
+ <?=$current->name?>
+ </h1>
+ <?= jvbFormatRating($current->term_id, 'term') ?>
+ <?= Render::renderFrom($meta, 'slogan'); ?>
+ </div>
+ </div>
+ </header>
+ <section>
+ <details class="bio-info">
+ <summary class="row x-btw">
+ <h2>Learn More About <?=$current->name?></h2>
+ </summary>
+ <div class="map">
+ <?= Render::renderFrom($meta, 'location'); ?>
+ </div>
+ <div class="short-bio">
+ <?= Render::renderFrom($meta, 'short_bio'); ?>
+ </div>
+
+ <div class="contact">
+ <h3>Contact:</h3>
+ <?php
+ echo jvbRenderContactInfo($current->term_id, 'term');
+ echo jvbRenderLinks($current->term_id, 'term');
+ ?>
+ </div>
+
+ <div id="about">
+ <?= Render::renderFrom($meta, 'bio')?>
+ </div>
+ </details>
+ </section>
+ <section id="contact" class="">
+ <h2>Contact </h2>
+ <?php
+ echo jvbRenderContactInfo($current->term_id, 'term');
+ echo jvbRenderLinks($current->term_id, 'term');
+ ?>
+ </section>
+ <?= jvbRenderHours($current->term_id, 'term')?>
+
+
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
+
+
+function jvbRenderTermSummary()
+{
+ $current = get_queried_object();
+ $cache = Cache::for('term_summary', WEEK_IN_SECONDS)->connect('taxonomy');
+ $key = $current->ID;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+ $tax = jvbNoBase($current->taxonomy);
+ switch ($tax) {
+ case 'style':
+ $title = 'Tattoo Artists';
+ break;
+ case 'theme':
+ $title = 'Tattoos';
+ break;
+ default:
+ $title = '';
+ }
+
+ $meta = Meta::forTerm($current->ID);
+ $fields = $meta->getAll();
+
+ ?>
+ <header id="top">
+ <h1><?= get_the_archive_title() ?></h1>
+ </header>
+
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
diff --git a/src/fields/save.js b/src/fields/save.js
new file mode 100644
index 0000000..8169594
--- /dev/null
+++ b/src/fields/save.js
@@ -0,0 +1,3 @@
+export default function save() {
+ return null; // Dynamic block rendered by PHP
+}
diff --git a/src/fields/style.scss b/src/fields/style.scss
new file mode 100644
index 0000000..b182fe9
--- /dev/null
+++ b/src/fields/style.scss
@@ -0,0 +1,20 @@
+details > div {
+ margin: 1rem 0;
+}
+
+main > header:not(:has(img)) {
+ margin-top: 3rem!important;
+}
+
+header a::before {
+ display: none!important;
+}
+
+header + details {
+ margin: 1.5rem auto 3rem!important;
+ max-width: var(--wide);
+}
+
+main {
+ padding-top: 0!important;
+}
diff --git a/src/fields/view.js b/src/fields/view.js
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/fields/view.js
@@ -0,0 +1 @@
+
diff --git a/src/forms/block.json b/src/forms/block.json
new file mode 100644
index 0000000..7279b38
--- /dev/null
+++ b/src/forms/block.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/forms",
+ "title": "Contact Forms",
+ "category": "jvb",
+ "icon": "align-center",
+ "description": "Our custom contact forms",
+ "keywords": [ "form", "forms", "contact" ],
+ "version": "1.0.0",
+ "textdomain": "jvb",
+ "attributes": {
+ "formType": {
+ "type": "string",
+ "default": ""
+ },
+ "showLabels": {
+ "type": "boolean",
+ "default": true
+ },
+ "customEmailTo": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "selectors": {
+ "root": ".jvb-form-block"
+ },
+ "styles": [
+ { "name": "default", "label": "Default", "isDefault": true }
+ ],
+ "example": {
+ "attributes": {
+ "formType": "contact",
+ "showLabels": true
+ }
+ },
+ "render": "file:./render.php",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./editor.scss",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/forms/edit.js b/src/forms/edit.js
new file mode 100644
index 0000000..3a8387b
--- /dev/null
+++ b/src/forms/edit.js
@@ -0,0 +1,319 @@
+/**
+ * edit.js
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import {
+ PanelBody,
+ SelectControl,
+ ToggleControl,
+ TextControl,
+ Notice
+} from '@wordpress/components';
+import { useState, useEffect } from '@wordpress/element';
+
+/**
+ * Styles
+ */
+import './editor.scss';
+
+/**
+ * Edit function for Form Block
+ */
+export default function Edit({ attributes, setAttributes }) {
+ const {
+ formType,
+ showLabels,
+ customEmailTo,
+ turnstileEnabled
+ } = attributes;
+
+ const [isPreviewVisible, setIsPreviewVisible] = useState(true);
+
+ const blockProps = useBlockProps({
+ className: `jvb-form-block ${formType ? `jvb-form-block-${formType}` : ''}`
+ });
+
+ // Get form types from localized data, with fallback
+ const getFormTypes = () => {
+ if (typeof window !== 'undefined' && window.jvbFormsData && window.jvbFormsData.formTypes) {
+ return window.jvbFormsData.formTypes;
+ }
+
+ // Fallback if data isn't available
+ return [
+ { label: __('Select a form type', 'jvb'), value: '' },
+ { label: __('No forms available', 'jvb'), value: '', disabled: true }
+ ];
+ };
+
+ // Get available forms configuration
+ const getAvailableForms = () => {
+ console.log(window.jvbFormsData);
+ if (typeof window !== 'undefined' && window.jvbFormsData && window.jvbFormsData.availableForms) {
+ return window.jvbFormsData.availableForms;
+ }
+ return {};
+ };
+
+ const formTypes = getFormTypes();
+ const availableForms = getAvailableForms();
+
+ // Get form configuration based on selected type
+ const getCurrentFormConfig = () => {
+ if (!formType || !availableForms[formType]) {
+ return null;
+ }
+ return availableForms[formType];
+ };
+
+ // Form labels based on the selected form type
+ const getFormLabels = () => {
+ const formConfig = getCurrentFormConfig();
+
+ if (formConfig) {
+ return {
+ title: formConfig.title || __('Form', 'jvb'),
+ description: Array.isArray(formConfig.description)
+ ? formConfig.description.join(' ')
+ : (formConfig.description || ''),
+ button: formConfig.submit || __('Submit', 'jvb')
+ };
+ }
+
+ return {
+ title: __('Form', 'jvb'),
+ description: formType ? __('Loading form configuration...', 'jvb') : __('Please select a form type in the sidebar', 'jvb'),
+ button: __('Submit', 'jvb')
+ };
+ };
+
+ const formLabels = getFormLabels();
+
+ // Render a preview of the form in the editor
+ const renderFormPreview = () => {
+ if (!formType) {
+ return (
+ <Notice status="warning" isDismissible={false}>
+ {__('Please select a form type in the block settings.', 'jvb')}
+ </Notice>
+ );
+ }
+
+ let formFields = [];
+
+ switch (formType) {
+ case 'contact':
+ formFields = [
+ { id: 'name', label: __('Name', 'jvb'), type: 'text', required: true },
+ { id: 'email', label: __('Email', 'jvb'), type: 'email', required: true },
+ { id: 'phone', label: __('Phone', 'jvb'), type: 'tel', required: true },
+ { id: 'instagram', label: __('Instagram URL', 'jvb'), type: 'url' },
+ { id: 'contact_methods', label: __('Preferred Contact', 'jvb'), type: 'checkboxes',
+ options: [
+ { value: 'text', label: __('Text', 'jvb') },
+ { value: 'call', label: __('Call', 'jvb') },
+ { value: 'email', label: __('Email', 'jvb') },
+ { value: 'instagram', label: __('Instagram', 'jvb') }
+ ]
+ },
+ { id: 'message', label: __('Your Message', 'jvb'), type: 'textarea', required: true }
+ ];
+ break;
+
+ case 'feature_request':
+ formFields = [
+ { id: 'name', label: __('Name', 'jvb'), type: 'text', help: __('Required if you want us to follow up.', 'jvb') },
+ { id: 'email', label: __('Email', 'jvb'), type: 'email', help: __('Required if you want us to follow up.', 'jvb') },
+ { id: 'follow_up', label: __('Would you like me to follow up with you?', 'jvb'), type: 'checkbox' },
+ { id: 'target_audience', label: __('This Feature is For', 'jvb'), type: 'checkboxes',
+ options: [
+ { value: 'artists', label: __('Artists', 'jvb') },
+ { value: 'visitors', label: __('Site Visitors', 'jvb') },
+ { value: 'partners', label: __('Partners', 'jvb') },
+ { value: 'other', label: __('Other', 'jvb') }
+ ]
+ },
+ { id: 'feature_name', label: __('Name your Feature', 'jvb'), type: 'text', required: true },
+ { id: 'message', label: __('Describe Your Feature', 'jvb'), type: 'textarea', required: true }
+ ];
+ break;
+
+ case 'technical_issue':
+ formFields = [
+ { id: 'name', label: __('Name', 'jvb'), type: 'text' },
+ { id: 'email', label: __('Email', 'jvb'), type: 'email' },
+ { id: 'follow_up', label: __('Would you like me to follow up with you?', 'jvb'), type: 'checkbox' },
+ { id: 'issue_type', label: __('Type of Issue', 'jvb'), type: 'checkboxes',
+ options: [
+ { value: 'visual', label: __('Visual', 'jvb') },
+ { value: 'error', label: __('Error Page', 'jvb') },
+ { value: 'other', label: __('Other', 'jvb') }
+ ]
+ },
+ { id: 'message', label: __('Please describe the issue.', 'jvb'), type: 'textarea', required: true }
+ ];
+ break;
+ }
+
+ return (
+ <>
+ <h3 className="jvb-form-title">{formLabels.title}</h3>
+ <p className="jvb-form-description">{formLabels.description}</p>
+
+ <div className="jvb-form-preview">
+ {formFields.map((field) => (
+ <div key={field.id} className="jvb-form-field">
+ {showLabels && (
+ <label htmlFor={`jvb-${field.id}`} className={field.required ? 'required' : ''}>
+ {field.label}
+ </label>
+ )}
+
+ {field.type === 'text' && (
+ <input
+ type="text"
+ id={`jvb-${field.id}`}
+ placeholder={showLabels ? '' : field.label}
+ disabled
+ />
+ )}
+
+ {field.type === 'email' && (
+ <input
+ type="email"
+ id={`jvb-${field.id}`}
+ placeholder={showLabels ? '' : field.label}
+ disabled
+ />
+ )}
+
+ {field.type === 'tel' && (
+ <input
+ type="tel"
+ id={`jvb-${field.id}`}
+ placeholder={showLabels ? '' : field.label}
+ disabled
+ />
+ )}
+
+ {field.type === 'url' && (
+ <input
+ type="url"
+ id={`jvb-${field.id}`}
+ placeholder={showLabels ? '' : field.label}
+ disabled
+ />
+ )}
+
+ {field.type === 'textarea' && (
+ <textarea
+ id={`jvb-${field.id}`}
+ placeholder={showLabels ? '' : field.label}
+ disabled
+ rows="4"
+ ></textarea>
+ )}
+
+ {field.type === 'checkbox' && (
+ <div className="jvb-form-checkbox">
+ <input
+ type="checkbox"
+ id={`jvb-${field.id}`}
+ disabled
+ />
+ <label htmlFor={`jvb-${field.id}`}>{field.label}</label>
+ </div>
+ )}
+
+ {field.type === 'checkboxes' && field.options && (
+ <div className="jvb-form-checkboxes">
+ {field.options.map((option) => (
+ <div key={option.value} className="jvb-form-checkbox">
+ <input
+ type="checkbox"
+ id={`jvb-${field.id}-${option.value}`}
+ disabled
+ />
+ <label htmlFor={`jvb-${field.id}-${option.value}`}>{option.label}</label>
+ </div>
+ ))}
+ </div>
+ )}
+
+ {field.help && (
+ <p className="jvb-form-help">{field.help}</p>
+ )}
+ </div>
+ ))}
+
+ {turnstileEnabled && (
+ <div className="jvb-form-turnstile">
+ <div className="jvb-turnstile-placeholder">
+ <span>{__('Cloudflare Turnstile will appear here', 'jvb')}</span>
+ </div>
+ </div>
+ )}
+
+ <div className="jvb-form-submit">
+ <button type="button" className="jvb-form-button">{formLabels.button}</button>
+ </div>
+ </div>
+ </>
+ );
+ };
+
+ return (
+ <>
+ <InspectorControls>
+ <PanelBody title={__('Form Settings', 'jvb')}>
+ <SelectControl
+ label={__('Form Type', 'jvb')}
+ value={formType}
+ options={formTypes}
+ onChange={(value) => setAttributes({ formType: value })}
+ />
+
+ <ToggleControl
+ label={__('Show Field Labels', 'jvb')}
+ checked={showLabels}
+ onChange={(value) => setAttributes({ showLabels: value })}
+ help={__('Toggle to show or hide field labels.', 'jvb')}
+ />
+
+ <TextControl
+ label={__('Custom Recipient Email', 'jvb')}
+ value={customEmailTo || ''}
+ onChange={(value) => setAttributes({ customEmailTo: value })}
+ help={__('Leave empty to use the default email.', 'jvb')}
+ type="email"
+ />
+ </PanelBody>
+
+ <PanelBody title={__('Form Preview', 'jvb')} initialOpen={false}>
+ <ToggleControl
+ label={__('Show Preview', 'jvb')}
+ checked={isPreviewVisible}
+ onChange={(value) => setIsPreviewVisible(value)}
+ />
+
+ <p className="components-base-control__help">
+ {__('This is just a preview. The actual form will be rendered on the frontend.', 'jvb')}
+ </p>
+ </PanelBody>
+ </InspectorControls>
+
+ <div {...blockProps}>
+ {isPreviewVisible ? (
+ renderFormPreview()
+ ) : (
+ <div className="jvb-form-placeholder">
+ <h3>{formLabels.title}</h3>
+ <p>{__('Form preview is hidden. Edit settings in the sidebar.', 'jvb')}</p>
+ </div>
+ )}
+ </div>
+ </>
+ );
+}
diff --git a/src/forms/editor.scss b/src/forms/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/forms/editor.scss
diff --git a/src/forms/index.js b/src/forms/index.js
new file mode 100644
index 0000000..fc49c90
--- /dev/null
+++ b/src/forms/index.js
@@ -0,0 +1,40 @@
+//index.js
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import save from './save';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+
+ /**
+ * @see ./save.js
+ */
+ save,
+} );
diff --git a/src/forms/index.php b/src/forms/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/forms/index.php
diff --git a/src/forms/render.php b/src/forms/render.php
new file mode 100644
index 0000000..9865bf3
--- /dev/null
+++ b/src/forms/render.php
@@ -0,0 +1,55 @@
+<?php
+if (!defined('ABSPATH')) {
+ exit; // Exit if accessed directly
+}
+/**
+ * Form Block Render
+ *
+ * @package Edmonton_Ink
+ */
+
+/**
+ * Renders the form block on the frontend
+ *
+ * @param array $attributes The block attributes.
+ * @param string $content The block content.
+ * @param WP_Block $block The block instance.
+ * @return string The rendered output.
+ */
+function jvbRenderFormBlock(array $attributes, string $content, WP_Block $block):string
+{
+ // Get form type from attributes
+ $form_type = isset($attributes['formType']) ? $attributes['formType'] : '';
+
+ // If no form type selected, return a message
+ if (empty($form_type)) {
+ return '<div class="jvb-form-error">No form type selected. Please edit this block and select a form type.</div>';
+ }
+
+ // Get other attributes
+ $show_labels = isset($attributes['showLabels']) ? $attributes['showLabels'] : true;
+ $custom_email_to = isset($attributes['customEmailTo']) ? $attributes['customEmailTo'] : '';
+
+ // Set custom options for the form
+ $form_options = array();
+
+ // Set custom email recipient if provided
+ if (!empty($custom_email_to)) {
+ $form_options['email_to'] = $custom_email_to;
+ }
+
+ // Render the form with the specified options
+ $form_output = JVB()->forms()->renderForm($form_type);
+
+ // Get block classes
+ $wrapper_attributes = get_block_wrapper_attributes([
+ 'class' => 'jvb-forms'
+ ]);
+
+ // Return the wrapped form
+ return sprintf(
+ '<div %1$s>%2$s</div>',
+ $wrapper_attributes,
+ $form_output
+ );
+}
diff --git a/src/forms/save.js b/src/forms/save.js
new file mode 100644
index 0000000..933c127
--- /dev/null
+++ b/src/forms/save.js
@@ -0,0 +1,23 @@
+//save.js
+/**
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * The save function defines the way in which the different attributes should
+ * be combined into the final markup, which is then serialized by the block
+ * editor into `post_content`.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
+ *
+ * @return {WPElement} Element to render.
+ */
+export default function save() {
+ // This is a dynamic block that is rendered on the server side
+ // Return null to let WordPress handle the saving and rendering
+ return null;
+}
diff --git a/src/forms/style.scss b/src/forms/style.scss
new file mode 100644
index 0000000..d540000
--- /dev/null
+++ b/src/forms/style.scss
@@ -0,0 +1,5572 @@
+//:target {
+// outline: none!important;
+// padding: 0!important;
+//}
+//:root {
+// --height: 3rem;
+//}
+//body:has(#theme-switcher:checked) {
+//
+//}
+//
+//.dashboard h1:first-of-type {
+// margin-top: 0!important;
+//}
+//main > footer {
+// max-width: 100%!important;
+// position: fixed;
+// z-index: 20;
+// bottom: 0;
+// left: 0;
+// right: 0;
+// width: 100%;
+// margin: 4rem 0 0 0!important;
+// height: var(--btn);
+// padding: 0;
+// background-color: var(--base);
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+//}
+//main>* {
+// max-width: min(768px, 90vw)!important;
+// margin: 0 auto!important;
+//}
+//main h1 {
+// margin: 0!important;
+// font-size: var(--txt-large);
+//}
+//main h1 + p + h2 {
+// font-size: var(--txt-medium);
+// text-transform: none;
+// margin: 0!important;
+//}
+//.replace {
+// max-width: 90vw!important;
+// margin: 1rem auto!important;
+//}
+//
+//.dash .replace {
+// text-align: center;
+//}
+//.dash input {
+// text-align: center;
+//}
+//.dash .true-false {
+// display: flex;
+// justify-content: center;
+// margin: 0;
+//}
+//.dash button[type=submit] {
+// width: 50%;
+// margin: 1rem auto;
+//}
+//
+//
+//form h2 {
+// margin: .5rem 0 1.5rem!important;
+//}
+//
+//.dashboard-nav {
+// height: var(--btn);
+// max-width:100vw;
+// padding: 0 .5rem;
+//}
+//.dashboard-nav ul {
+// height: var(--btn);
+// overflow-x: auto;
+//}
+//.dashboard-nav li + li:before {
+// display: none!important;
+//}
+//.dashboard-nav a {
+// height: var(--btn);
+// min-width: var(--btn);
+// padding: 0 .75rem;
+// color: var(--contrast)!important;
+//}
+//.dashboard-nav a .icon {
+// margin: 0;
+//}
+//.dashboard-nav a span {
+// display: none;
+//}
+//.dashboard-nav .current a:hover,
+//.dashboard-nav a:hover {
+// background-color: var(--action-0)!important;
+// color: var(--action-contrast)!important;
+//}
+//.dashboard-nav .current a {
+// background-color: var(--base-100)!important;
+// color: var(--contrast)!important;
+//}
+//.dashboard-nav .current a span {
+// display: block;
+//}
+//
+//
+//
+///* Loading states */
+//main {
+// opacity: 1;
+// transition: opacity .3s ease-in-out;
+// padding-bottom: 7rem;
+// min-height: calc(100vh - 12rem);
+//}
+//
+//main.transitioning {
+// opacity: 0;
+//}
+//.loading {
+// opacity: .7;
+//}
+//
+//
+///*Upload overlay*/
+//.loading-overlay {
+// position: fixed;
+// top: 0;
+// left: 0;
+// right: 0;
+// bottom: 0;
+// background-color: rgba(var(--base-rgb),var(--op-4));
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// opacity: 0;
+// visibility: hidden;
+// transition: opacity .3s ease, visibility .3s ease;
+// z-index: 9999;
+//}
+//
+//.loading-overlay.active {
+// opacity: 1;
+// visibility: visible;
+//}
+//
+//.loading-overlay .wrapper {
+// background-color: var(--base);
+// padding: 2rem;
+// border-radius: 8px;
+// text-align: center;
+// max-width: 90%;
+// width: 400px;
+//}
+//
+//.upload-spinner {
+// width: 50px;
+// height: 50px;
+// border: 5px solid var(--base-200);
+// border-top: 5px solid var(--action-0);
+// border-radius: 50%;
+// margin: 0 auto 1rem;
+// animation: spin 1s linear infinite;
+//}
+//
+//.upload-status h3 {
+// margin: 0 0 .5rem;
+// color: var(--contrast);
+//}
+//
+//.upload-message {
+// margin: 0;
+// color: var(--contrast-100);
+// font-size: var(--txt-x-small);
+//}
+//
+//
+//
+///* Optional: Add a pulsing effect to the text */
+//.upload-message {
+// animation: flicker 2s infinite;
+//}
+//
+//@keyframes flicker {
+// 0% { opacity: .6; }
+// 50% { opacity: 1; }
+// 100% { opacity: .6; }
+//}
+//
+//.form-section {
+// max-height: 0;
+// transform: scale(0);
+// visibility: hidden;
+//}
+//form:not(:has(.form-section.active)) .form-section:first-of-type,
+//.form-section.active {
+// max-height: fit-content;
+// transform: scale(1);
+// visibility: visible;
+//}
+//
+//
+//
+//.form-sections {
+// --height: fit-content;
+// position: fixed;
+// bottom: 3rem;
+// left: 0;
+// right: 0;
+// background-color: var(--base-100);
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// z-index: 10;
+//}
+//.form-sections ul {
+// gap: 0;
+//}
+//.form-sections li {
+// width: 50%;
+//}
+//
+//.form-sections a:visited,
+//.form-sections a {
+// padding: .25rem;
+// width: 100%;
+// color: var(--contrast);
+//}
+//.replace:not(:has(.form-section.active)) .form-sections li:first-of-type a,
+//.current a {
+// background-color: var(--base-200);
+// color: var(--contrast);
+//}
+//.form-sections .icon {
+// margin: 0;
+//}
+//@media (min-width: 768px){
+// .form-sections ul {
+// flex-wrap: nowrap;
+// }
+// .form-sections li {
+// width: 100%;
+// }
+//}
+//
+//.submit-container {
+// position: fixed;
+// z-index: 20;
+//}
+//.submit-container button {
+// padding: 0;
+// width: 3rem;
+// height: 3rem;
+// color: var(--contrast-200);
+// background-color: var(--base-200);
+// justify-content: center;
+// border: 3px solid transparent;
+//}
+//.submit-container .save-popup {
+// position: absolute;
+// z-index: -1;
+// top: calc(50% - (1.875rem / 2));
+// font-size: var(--txt-x-small);
+// background-color: var(--action-0);
+// color: var(--action-contrast);
+// padding: .25rem .5rem;
+// border-radius: 4px;
+// white-space: nowrap;
+// visibility: hidden;
+// transition: all var(--trans-base);
+// opacity: 0;
+//}
+//.submit-container .icon {
+// --w: 2em;
+//}
+//.save-popup::before {
+// content: '';
+// position: absolute;
+// top: 50%;
+// transform: translateY(-50%);
+// border-top: .5rem solid transparent;
+// border-bottom: .5rem solid transparent;
+//}
+//.save-popup.show {
+// opacity: 1;
+// visibility: visible;
+//}
+//
+//@media (max-width: 767px){
+// .submit-container {
+// top: 3.5rem;
+// right: .5rem;
+// }
+// .save-popup {
+// right: 0;
+// }
+// .save-popup:before{
+// right: -.25rem;
+// border-left: .5rem solid var(--action-0);
+// }
+// .save-popup.show {
+// right: calc(100% + .5rem);
+// }
+//}
+//@media (min-width: 768px){
+// .submit-container {
+// bottom: 6rem;
+// left: .5rem;
+// }
+// .save-popup {
+// left: 0;
+// }
+// .save-popup:before{
+// left: -.25rem;
+// border-right: .5rem solid var(--action-0);
+// }
+// .save-popup.show {
+// left: calc(100% + .5rem);
+// }
+//}
+//
+//.autosaving span.save,
+//.autosaving button[type=submit] {
+// border-color: var(--base-200);
+// border-top-color: var(--action-0);
+// border-bottom-color: var(--action-50);
+// border-radius: 50%;
+// color: var(--contrast-200);
+// transition: color .25s var(--trans-t) var(--trans-fn);
+// transition-property: color, background-color, border;
+// animation: spin 1s linear infinite;
+//}
+//.autosaving .submit-container {
+// animation: pulse 1s linear infinite;
+//}
+//
+//@keyframes spin {
+// 0% { transform: rotate(0deg); }
+// 100% { transform: rotate(360deg); }
+//}
+//@keyframes pulse {
+// 0% {
+// transform: scale(.85);
+// opacity: .6;
+// }
+// 50% {
+// transform:scale(1);
+// opacity: 1; }
+// 100% {
+// transform: scale(.85);
+// opacity: .6; }
+//}
+//
+//.item-link[href=""]{
+// display: none;
+//}
+//.item-link {
+// position: fixed;
+// bottom: 5rem;
+// right: 1.5rem;
+// text-transform: uppercase;
+// color: var(--action-0);
+// background-color: var(--base);
+// border-radius: 4px;
+// padding: .25rem .5rem;
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw-subtle);
+//}
+//.field {
+// margin: 3rem .5rem;
+//}
+//.field:has([required]) label::after {
+// content: 'required';
+// background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="%23B7332E" viewBox="0 0 256 256"><path d="M214.86,180.12a8,8,0,0,1-11,2.74L136,142.13V216a8,8,0,0,1-16,0V142.13L52.12,182.86a8,8,0,1,1-8.23-13.72L112.45,128,43.89,86.86a8,8,0,1,1,8.23-13.72L120,113.87V40a8,8,0,0,1,16,0v73.87l67.88-40.73a8,8,0,1,1,8.23,13.72L143.55,128l68.56,41.14A8,8,0,0,1,214.86,180.12Z"></path></svg>');
+// display: inline-block;
+// background-position: left center;
+// background-size: 1.2em;
+// padding-left: 1.3em;
+// background-repeat: no-repeat;
+// font-weight: 100;
+// text-transform: none;
+// font-size: .75em;
+// vertical-align: super;
+//
+//}
+//.field:has([required]) label {
+// position: relative;
+//}
+//.repeater-items .field {
+// margin: 1rem 0;
+//}
+//.taxonomy .field-group-header {
+// display: flex;
+// justify-content: space-between;
+//}
+//
+//.add-item-btn {
+// padding: .5em;
+// background: var(--base-100);
+// border: 1px solid var(--contrast-200);
+// border-radius: .25rem;
+// cursor: pointer;
+// font-size: .875rem;
+//}
+//.add-item-btn .icon {
+// --w: 1.5em;
+//}
+//
+//.add-item-btn:hover {
+// background: var(--base-200);
+//}
+//summary .type-label {
+// display: flex;
+// align-items: center;
+// gap: 1rem;
+//}
+//summary .type-label .icon {
+// position: relative;
+// top: -.25em;
+//}
+//
+///** Tabs **/
+//.tabs {
+// display: flex;
+// margin-bottom: 1.5rem;
+// border-bottom: 2px solid var(--base-200);
+// flex-wrap: wrap;
+//}
+//.tabs.parent {
+// max-width: 100vw;
+// background-color: var(--base);
+// padding: .5rem;
+// width: 100vw;
+// position: relative;
+// left: -2.1rem;
+// margin: 1rem 0;
+//}
+//.tab-content h2 {
+// font-size: var(--txt-large);
+// margin: 0!important;
+//}
+//.tab-content .tab-navigation,
+//.tab-content button[type=submit] {
+// display: inline-flex;
+// width: 48%;
+// background-color: var(--action-200);
+// color: var(--contrast-200);
+//}
+//
+//.tab-navigation.next {
+// display: flex;
+// margin-left: auto;
+//}
+//.tab-content .tab-navigation + button {
+// display: inline-flex;
+// margin-left: 3%;
+//}
+//
+//.tabs > button {
+// padding: .75rem 1.5rem;
+// border-radius: 0;
+// border: none;
+// background: none;
+// font-size: 1.1rem;
+// font-weight: bold;
+// letter-spacing: .05em;
+// text-transform: uppercase;
+// cursor: pointer;
+// position: relative;
+// color: inherit;
+//}
+//.tabs > button h2 {
+// font-size: 1.1rem;
+// line-height: 1;
+// margin: 0!important;
+//}
+//.tabs > button:hover,
+//.tabs > button:focus {
+// 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 {
+// width: 100%;
+//}
+//
+//.tabs > button.add-item-btn {
+// margin-left: auto;
+// background-color: var(--action-50);
+// border-radius: 4px;
+// padding: .5em;
+// font-weight: normal;
+// border: 1px solid var(--action-50);
+//}
+//.tabs > button.add-item-btn:focus,
+//.tabs > button.add-item-btn:hover {
+// background-color: var(--base);
+//}
+//
+//.type-filter:not(.active) span:not(.count){
+// display: none;
+//}
+//.type-filter:not(.active):hover span:not(.count){
+// display: block;
+//}
+///** News **/
+//.item-grid.list-view .item.news {
+// flex-direction: column;
+//}
+//.item.news h3 {
+// font-size: var(--txt-medium);
+// margin: 0!important;
+//}
+//.item.news summary .image {
+// width: 5rem;
+// background-color: var(--base-200);
+// border-radius: 4px;
+// aspect-ratio: 1;
+//}
+//.item.news summary {
+// gap: 1rem;
+//}
+//.item.news summary .summary {
+// width: 100%;
+//}
+//.item.news summary .header {
+// display: flex;
+// justify-content: space-between;
+// padding: .5rem 0;
+//}
+//.item .vote {
+// display: flex;
+// justify-content: flex-end;
+// align-items: center;
+// gap: .5em;
+// margin-top: auto;
+//}
+//.vote .count {
+// white-space: nowrap;
+//}
+//.item .vote .count:nth-of-type(2) {
+// margin-right: 2rem;
+//}
+///** Favourites **/
+//.favourites-title {
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+//}
+//main button {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+//}
+//
+//.my-lists {
+// margin: .5rem 0;
+//}
+//
+//details.uploader .file-upload-container {
+// margin: 1rem var(--mr) 1rem var(--ml);
+// max-width: var(--content);
+//}
+//details .no-items {
+// text-align: center;
+// font-style: italic;
+// background-color: var(--base-50);
+// padding: var(--p-outer);
+// border-radius: var(--radius);
+//}
+//
+//.controls {
+// display: flex;
+// flex-wrap: wrap;
+// gap: .5rem;
+// justify-content: space-between;
+//}
+//
+//.controls label .icon + span {
+// transform: scaleX(0);
+// max-width: 0;
+//}
+//
+//.type-filters:hover label:hover .icon + span,
+//.controls label:hover .icon + span,
+//.controls :checked + label .icon + span {
+// transform: scaleX(1);
+// max-width: fit-content;
+//}
+//.type-filters,
+//.view-controls {
+// display: flex;
+// flex-wrap: wrap;
+// gap: .5rem;
+//}
+//
+///** Generalized Type Filters **/
+//details.type-filters {
+// flex-direction: column;
+//}
+//details.type-filters form.filters {
+// flex-direction: column;
+//}
+//
+//details.type-filters div.filters {
+// flex-wrap: wrap;
+//}
+//
+//details.type-filters .order-options {
+// width: 100%;
+// display: flex;
+//}
+//details.type-filters .order-options > div {
+// display: flex;
+//}
+//details.type-filters .order-by {
+// width: 100%;
+//}
+//details.type-filters .order-by .radio-group-label {
+// width: 100%;
+// display: flex;
+// gap: 1rem;
+//}
+//details.type-filters label {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+//}
+//
+//details.type-filters label {
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// padding: .35rem;
+// white-space: nowrap;
+// width: fit-content;
+// height: fit-content;
+// cursor: pointer;
+// border: 1px solid var(--base-200);
+// border-radius: 4px;
+// font-size: .875rem;
+// transition: border-color var(--trans-base);
+// margin-bottom: .5rem;
+//}
+//.filter-toggle .icon {
+// margin-right: .5rem;
+//}
+//details.type-filters label:hover,
+//details.type-filters input:checked + label {
+// background-color: var(--light-0);
+// border-color: var(--action-0);
+// color: var(--action-0);
+//}
+//details.type-filters .order-direction {
+// justify-content: flex-end;
+//}
+//
+///** Generalized Modal **/
+//
+///* Grid */
+//.create-item {
+// left: auto!important;
+// right: 1rem;
+// bottom: calc(var(--btn) + 1rem)!important;
+//}
+//body:has(.group-display:not([hidden])) button.create-item{
+// display: none;
+//}
+//.uploader .groups,
+//.item-grid {
+// --padding: 0;
+// padding: var(--padding);
+// transition: padding var(--trans-base);
+//}
+//.uploader .groups,
+//.item-grid:not(.list-view) {
+// display: grid;
+// grid-template-columns: repeat(2, 50%);
+// gap: .5rem;
+// margin-top: 2rem;
+//}
+//.item-grid.empty {
+// grid-template-columns: repeat(1, 1fr)!important;
+//}
+//.item-grid.empty div {
+// text-align: center;
+// border-radius: var(--radius);
+// background-color: var(--base-100);
+//}
+//.item-grid.empty h3 .icon {
+// display: inline-flex;
+// vertical-align: middle;
+// margin: 0 1rem;
+//}
+//.uploader .group,
+//.item {
+// width: calc(100% - (var(--padding) * 2));
+//}
+//.item-grid .item {
+// user-select: none;
+// -webkit-user-select: none;
+// -moz-user-select: none;
+// -ms-user-select: none;
+//}
+//.item-grid .item .logo {
+// --w: 100%;
+// opacity: .2;
+//}
+///** Grid View **/
+//.item-grid:not(.list-view) .item {
+// display: flex;
+// position: relative;
+// flex-direction: column;
+//}
+//.item-grid:not(.list-view) button.favourite,
+//.item-grid:not(.list-view) .item-select label {
+// position: absolute;
+// width: 3em;
+// height: 3em;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// top: .125rem;
+// padding: 0!important;
+// border-radius: var(--radius);
+// background-color: rgba(var(--base-rgb),var(--op-3));
+// color: var(--base-200);
+//}
+//.item-grid:not(.list-view) button.favourite:hover,
+//.item-grid:not(.list-view) .item-select label:hover {
+// background-color: rgba(var(--base-rgb),var(--op-6));
+// color: var(--contrast);
+//}
+//.item-grid:not(.list-view) .item-select label::before {
+// border-color: var(--base-200);
+// position: relative;
+// top: 0;
+// left: 0;
+// transform: none;
+//}
+//
+//.item-grid:not(.list-view) .item-select label::after {
+// left: calc(50% - 4px)!important;
+//}
+//.item-grid:not(.list-view) .item-select label:hover::before {
+// border-color: var(--contrast);
+//}
+//.item-grid:not(.list-view) .item-select label {
+// left: .1255rem;
+//}
+//.item-grid:not(.list-view) button.favourite {
+// right: .125rem;
+//}
+//.item-grid:not(.list-view) img {
+// width: 100%;
+// max-width: 100%;
+// height: auto;
+// aspect-ratio: 1;
+// object-fit: cover;
+//}
+///** List View **/
+//.replace:not(:has(.list-view)) button[data-view=grid],
+//.replace:has(.list-view) button[data-view=list] {
+// background-color: var(--base);
+//}
+//.item-grid.list-view .item {
+// display: flex;
+// position: relative;
+// flex-direction: row;
+// margin: .5rem 0;
+// padding: .25rem .5rem;
+// gap: .5rem;
+//}
+//.item-grid.list-view .item:nth-of-type(even){
+// background-color: var(--base-100);
+//}
+//
+//.item-grid.list-view .item-select label{
+// display: flex;
+// height: 100%;
+// justify-content: center;
+// align-items: center;
+// padding: 0 .5rem!important;
+//}
+//.item-grid.list-view .item-select label::after {
+// /*left: calc(50% - 4px)!important;*/
+//}
+//.item-grid.list-view .item-select label::before {
+// position: relative;
+// top: 0;
+// transform: none;
+//}
+//.item-grid.list-view .item-select label:hover {
+// background-color: var(--base);
+//}
+//
+//.item-grid.list-view .item-select label:hover::before {
+// border-color: var(--action-0);
+//}
+//.item-grid.list-view img {
+// width: 200px;
+// height: 200px;
+// aspect-ratio: 1;
+// object-fit: cover;
+//}
+//.item-grid.list-view .item-info {
+// width: 100%;
+//}
+//.item-grid .item-info h3 {
+// margin: 0!important;
+// text-align: right;
+// font-size: var(--txt-medium);
+// text-transform: none;
+//}
+//.item-grid .item-info a {
+// text-align: right;
+// width: 100%;
+// display: block;
+// font-weight: normal;
+// font-family: var(--body);
+//}
+//.item-grid .item-info a::before,
+//.item-grid .item-info a::after {
+// display: none;
+//}
+//.item-grid.list-view .item-info .toggle-notes {
+// display: none;
+//}
+//.item-grid.list-view .item-info .notes-content[hidden]{
+// display: block!important;
+//}
+//.item-grid.list-view .item-info .notes-content {
+// margin-top: 1rem;
+//}
+//.no-favourites {
+// text-align: center;
+//}
+//.no-favourites h3 {
+// margin: 0 auto!important;
+//}
+///**
+//Limit reached
+// */
+//.reached .current {
+// color: var(--action-50);
+//}
+//.reached textarea,
+//.reached input {
+// border: 2px solid var(--action-50);
+//}
+///**
+//Groups
+// */
+//.groups {
+// gap: 1rem;
+//}
+//details.uploaded .file-upload-container,
+//details:not(.uploaded) .selection-container {
+// display: none;
+//}
+//.uploading + .selection-container {
+// display: flex!important;
+//}
+//.selection-container #save-changes {
+// position: sticky;
+// top: 3rem;
+// left: 100%;
+// border: 1px solid transparent;
+// background-color: var(--action-50);
+// box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// z-index: 5;
+//}
+//.selection-container #save-changes:hover {
+// background-color: var(--base);
+// border: 1px solid var(--action-50);
+// color: 1px solid var(--action-50);
+//}
+//
+//.group {
+// padding: 1rem .66rem;
+// background-color: var(--base-50);
+// border-radius: var(--radius-outer);
+//}
+//.group.empty {
+// aspect-ratio: 1;
+// display: flex;
+// flex-direction: column;
+// align-items: center;
+// justify-content: center;
+// border: 4px dashed var(--base-200);
+//}
+//.group.empty .items {
+// padding: 0;
+// margin-top: 0;
+//}
+//
+//.group .group-header {
+// display: flex;
+// position: relative;
+//}
+//.group .group-header label {
+// position: absolute;
+// top: -1.5rem;
+//}
+//.group .items {
+// margin-top: 1rem;
+// padding: 1rem;
+// border-radius: var(--radius);
+// background-color: var(--base);
+//}
+//.group .item-actions {
+// display: flex;
+// justify-content: space-between;
+// width: 100%;
+//}
+//.group .item-actions > button,
+//.group .item-actions label {
+// flex: 1;
+// padding: .25em!important;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// margin-bottom: 0;
+//}
+//.group .item-actions :checked + label {
+// background-color: var(--action-50);
+// color: var(--contrast);
+//}
+//.group .item-actions :checked + label:hover {
+// border: 1px solid var(--action-50);
+// background-color: transparent;
+// color: var(--action-50);
+//}
+//.group .item-actions label::before {
+// display: none!important;
+//}
+//
+//.group-item img,
+//.preview-item img {
+// width: 250px;
+// height: auto;
+// object-fit: cover;
+// aspect-ratio: 1;
+//
+//}
+//.uploading .preview-item img,
+//.uploading .group-item img{
+// animation: uploading 3s ease-in-out infinite alternate;
+//}
+//@keyframes uploading {
+// 0% {
+// opacity: .6;
+// filter: sepia(.3) grayscale(.3);
+// transform: scale(.9);
+// }
+// 100% {
+// opacity: 1;
+// filter: sepia(0) grayscale(0);
+// transform: scale(1);
+// }
+//}
+//
+//.image-actions {
+// display: flex;
+// gap: 1rem;
+// flex-wrap: wrap;
+//}
+//
+//.group.drop {
+// border: 2px dashed var(--action-0);
+// background: var(--action-rgb-subtle);
+//}
+//
+//.group .items {
+// padding: 1rem;
+// display: grid;
+// grid-template-columns: repeat(3, 1fr);
+// gap: 1rem;
+//}
+//
+//.group-content {
+// min-height: 100px;
+// padding: 1rem;
+//}
+//
+//.group.drop {
+// background: var(--action-rgb-subtle);
+// border: 2px dashed var(--action-0);
+//}
+//.gallery .group-item,
+//.gallery .preview-item {
+// cursor: grab;
+//}
+//.group-item.dragging,
+//.preview-item.dragging {
+// cursor: grabbing;
+// opacity: .5;
+//}
+//
+//.items {
+// display: grid;
+// grid-template-columns: repeat(3,1fr);
+// gap: 1rem;
+//}
+//
+///** Gallery **/
+//.gallery .preview-item .move-image {
+// cursor: grab;
+// position: absolute;
+// top: .5rem;
+// left: .5rem;
+// opacity: 0;
+// transition: opacity .2s;
+//}
+//.gallery-preview {
+// display: grid;
+// grid-template-columns: repeat(3, 1fr);
+// padding: .5rem;
+// background-color: var(--base-100);
+// border-radius: var(--radius-outer);
+//}
+//.gallery-preview .preview-item {
+// padding: .5rem;
+// background-color: var(--base);
+// border-radius: var(--radius);
+//}
+//
+//.gallery .preview-item:hover .move-image {
+// opacity: 1;
+//}
+//
+//.gallery .preview-item.sortable-ghost {
+// opacity: .5;
+//}
+//
+//.gallery .preview-item.sortable-drag {
+// cursor: grabbing;
+//}
+//
+//.preview-item .upload-status {
+// position: absolute;
+// bottom: 0;
+// left: 0;
+// right: 0;
+// height: 4px;
+// background: rgba(0,0,0,.1);
+//}
+//
+//.preview-item.error {
+// border-color: red;
+//}
+//
+//.preview-item.complete .upload-status {
+// display: none;
+//}
+//
+///** Settings **/
+//.notification-table {
+// width: 100%;
+//}
+//.notification-table tr td:first-of-type {
+// width: 100%;
+//}
+//.notification-table td {
+// vertical-align: middle;
+// padding: 0 .5rem;
+//}
+//.notification-table td + td {
+// text-align: right;
+//}
+//table tr:nth-of-type(even){
+// background-color: var(--base-200);
+//}
+//thead th {
+// width: 50%;
+//}
+//thead tr,
+//tfoot tr {
+// background-color: var(--base);
+// text-transform: uppercase;
+// padding: .5rem 0;
+// line-height: 2;
+// font-weight: normal;
+//}
+//tfoot th {
+// vertical-align: middle;
+//}
+//tfoot th:first-of-type {
+// text-align: right;
+//}
+//@media (max-width: 767px){
+// table .radio-options {
+// display: grid;
+// grid-template-columns: repeat(2, 1fr);
+// }
+// table .radio-options label {
+// width: 85%;
+// margin-bottom: 0;
+// }
+//}
+//
+///** Favourites Lists **/
+//.list-card {
+// background-color: var(--base-50);
+// padding: 1rem;
+// border-radius: var(--radius);
+//}
+//.list-header {
+// display: flex;
+// justify-content: space-between;
+//}
+//h2 .icon {
+// --w: 1.5em;
+// display: inline-block;
+// vertical-align: middle;
+// margin-right: .75rem;
+//}
+//.list-card h3,
+//.list-header h2 {
+// margin: 0!important;
+// font-size: var(--txt-large);
+//}
+//.list-actions {
+// display: flex;
+// justify-content: space-between;
+// gap: .5rem;
+//}
+//.list-header + .list-actions {
+// justify-content: flex-end;
+//}
+//.create-list-btn {
+// font-size: var(--txt-x-small);
+//}
+//.meta-stats {
+// display: flex;
+// justify-content: space-between;
+// font-style: italic;
+//}
+//
+///** Repeater **/
+//.add-repeater-row {
+// margin-left: auto;
+// border: 1px solid var(--contrast-200);
+//}
+//
+///** Image **/
+//
+//.image-display {
+// max-height: 0;
+// overflow: hidden;
+// transform: scaleY(0);
+// transition: max-height var(--trans-t) var(--trans-fn);
+// transition-property: max-height, transform;
+//}
+//.image-display.has-image {
+// max-height: 100%;
+// transform: scaleY(1);
+// transition: max-height var(--trans-t) var(--trans-fn);
+// transition-property: max-height, transform;
+//}
+//.file-upload-container {
+// margin-top: 1rem;
+//}
+//
+//.file-upload-wrapper {
+// border: 2px dashed var(--action-0);
+// border-radius: 4px;
+// padding: 2rem;
+// text-align: center;
+// transition: all .3s ease;
+// background: var(--action-rgb-subtle);
+// position: relative;
+// cursor: pointer;
+//}
+//.file-upload-wrapper h2 {
+// margin: 0!important;
+// font-size: var(--txt-large);
+//}
+//
+//.file-upload-wrapper:hover,
+//.dragover {
+// background: var(--action-rgb-subtle-hover);
+// 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;
+//}
+//
+///* Error state */
+//.file-error {
+// color: var(--action-0);
+// margin-top: .5rem;
+// font-size: .9em;
+//}
+//
+//.file-upload-container.uploading,
+//.image-display.has-image + .file-upload-container {
+// max-height: 0;
+// overflow: hidden;
+// transform: scaleY(0);
+// transition: max-height var(--trans-t) var(--trans-fn), transform var(--trans-t) var(--trans-fn);
+// transition-property: max-height, transform;
+//}
+//
+///** Highlighted Taxonomy **/
+//.highlight-options {
+// display: grid;
+// grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+// gap: .5rem;
+// margin-top: 1rem;
+//}
+//
+//.highlight-option {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// padding: .5rem;
+// border: 1px solid #ddd;
+// border-radius: 4px;
+//}
+//.highlight-option label {
+// margin-bottom: 0;
+// font-weight: normal;
+//}
+//
+//.highlight-option input[type=checkbox]:disabled + label {
+// opacity: .5;
+// cursor: not-allowed;
+//}
+//
+///** Item Selection **/
+//
+//.item-grid {
+// --padding: 0;
+// padding: var(--padding);
+//}
+//.item-grid.selecting {
+// --padding: .5rem;
+// transition: none;
+// background-color: var(--base);
+//}
+//.item {
+// --padding: 0;
+// height: fit-content;
+// padding: var(--padding);
+// max-width: calc(100% - (var(--padding) * 2));
+// transition: padding var(--trans-base);
+//}
+//.selecting .item {
+// opacity: .666;
+//}
+//.selecting .item:has(.select-checkbox:checked) {
+// --padding: .5rem;
+// opacity: 1;
+// background-color: var(--action-0);
+// transition: none;
+//}
+//
+//
+//.item-grid.preview .preview-item {
+// display: flex;
+// gap: 2rem;
+//}
+//.preview-item .preview-image {
+// width: 36%;
+//}
+//.preview-item .remove-file {
+// width: 100%;
+// background-color: var(--base);
+// color: var(--contrast);
+//}
+//.preview-item .field:first-of-type {
+// margin-top: 0!important;
+//}
+//.preview-item + .preview-item {
+// margin-top: 2rem;
+// padding-top: 2rem;
+// border-top: 2px solid var(--contrast-200);
+//}
+//
+//.item-grid.preview:empty + .hint {
+// display: none;
+//}
+//
+//body:has(.item-grid.empty) .bulk-controls {
+// display: none;
+//}
+//.bulk-controls {
+// margin: .75rem 0;
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+//}
+//.bulk-select {
+// display: flex;
+// justify-content: space-between;
+//}
+//
+//.bulk-actions[hidden]{
+// max-width: 0;
+// max-height: 0;
+// overflow: hidden;
+// transform: scaleY(0);
+// transition: transform var(--trans-t) var(--trans-fn);
+// transition-property: transform, max-height;
+// transform-origin: top;
+// display: flex!important;
+// align-items: center;
+// gap: .5rem;
+// position: relative;
+//}
+//
+//.bulk-actions {
+// max-width: 100%;
+// max-height: 100%;
+// transform: scaleY(1);
+// transition: transform var(--trans-t) var(--trans-fn);
+// transition-property: transform, max-height;
+// overflow:visible;
+// transform-origin: top;
+// display: flex;
+// gap: .25rem;
+//}
+//.bulk-actions select {
+// margin-right: .5rem;
+//}
+//
+//.selected-count {
+// font-size: var(--txt-x-small);
+// font-style: italic;
+// font-weight: normal;
+// margin-left: 1rem;
+//}
+//
+//.input-with-button {
+// display: flex;
+// gap: .5rem;
+//}
+//.input-with-button .icon {
+// --w: 1.5em;
+//}
+///** Single Edit Modal **/
+//dialog > form .field {
+// margin: 2rem 0;
+//}
+//.edit-modal [data-field=featured_image] img {
+// width: 100%;
+// height: 100%;
+// object-fit: cover;
+// aspect-ratio: 1;
+//}
+//
+///** Bulk Edit Modal **/
+//.bulk-edit-modal .selected {
+// display: grid;
+// grid-template-columns: repeat(5, 1fr);
+// gap: .5rem;
+// background-color: var(--base);
+// padding: .5rem;
+// border-radius: var(--radius-outer);
+//}
+//.bulk-edit-modal .selected input[type=checkbox] {
+// position: absolute;
+//}
+//.bulk-edit-modal .selected label::before,
+//.bulk-edit-modal .selected label::after {
+// display: none;
+//}
+//.bulk-edit-modal .selected label {
+// opacity: .5;
+// padding: .5rem!important;
+// aspect-ratio: 1;
+// margin-bottom: 0;
+// max-width: 5rem;
+// height: auto;
+// cursor: pointer;
+//}
+//.bulk-edit-modal .selected label:has(input:checked) {
+// opacity: 1;
+// padding: 0!important;
+//}
+//
+//.bulk-edit-modal .selected img {
+// max-width: 100%;
+// object-fit: cover;
+// aspect-ratio: 1;
+// border-radius: var(--radius);
+//}
+//
+//dialog .term-list {
+// display: block;
+//}
+//.pagination-info {
+// position: sticky;
+// background-color: rgba(var(--base-rgb),var(--op-6));
+// top: 0;
+//}
+//.pagination-info:empty {
+// display: none;
+//}
+//
+///** Quill Stuff **/
+//.editor-container {
+//
+//}
+//.editor-container .ql-toolbar {
+// display: flex;
+// justify-content: flex-start;
+// flex-wrap: wrap;
+// padding: .25rem;
+// gap: 1rem;
+// background-color: var(--base);
+// 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;
+//}
+//.editor-container .ql-container .ql-editor {
+// padding: var(--padding);
+// width: calc(100% - (var(--padding) * 2.5));
+// height: calc(100% - (var(--padding) * 2));
+//}
+//.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: 0px 0px 5px rgba(var(--base-rgb),var(--op-6));
+// color: var(--contrast);
+// padding: 5px 12px;
+// white-space: nowrap;
+//}
+//
+//#toolbar-einks_short_bio .ql-e_image {
+// display: none;
+//}
+//
+///** Content Grid **/
+//.all-filters {
+// position: relative;
+// background-color: var(--base);
+// border-radius: var(--radius-outer);
+// padding: .5rem;
+// display: flex;
+// flex-direction: column;
+// gap: .5rem;
+//}
+//.controls .radio-options {
+// gap: .25rem 1rem;
+//}
+//.radio-options details {
+// width: 100%;
+//}
+//.radio-options input:not(.ch) + label,
+//.controls input:not(.ch) + .radio-options label {
+// width: fit-content!important;
+// gap: 0;
+// margin: 0;
+// display: flex;
+// padding: .25rem .5rem!important;
+// align-items: center;
+//}
+//
+//.radio-options label span {
+// transform: scaleX(0);
+// max-width: 0;
+//}
+//.edit-form .radio-options label span,
+//.radio-options label:hover span,
+//.radio-options :checked + label span {
+// transform: scaleX(1);
+// max-width: 100%;
+//}
+//.radio-options input:not(.ch):checked + label {
+// padding: .25rem .5rem!important;
+//}
+//.controls .radio-options input:not(.ch):checked+label {
+// background-color: var(--base-100);
+// border-color: var(--contrast-200);
+// color: var(--contrast)!important;
+// gap: .5rem;
+//}
+//
+//dialog .field {
+// margin: 3rem 0;
+//}
+//
+//
+//.filters {
+// display: flex;
+// gap: 1rem;
+// align-items: center;
+//}
+//.filters > div > label {
+// margin: 0;
+//}
+//div.filters > div {
+// display: flex;
+// align-items: center;
+// flex: 100%;
+// gap: .5rem;
+//}
+//.filters .icon {
+// --w: 1.5em;
+//}
+//
+//@media (min-width:768px){
+// div.filters > div {
+// flex: 1;
+// }
+//}
+//
+//.view-controls.radio-options {
+// gap: .5rem;
+// margin-left: auto;
+//}
+//.view-controls.radio-options label {
+// padding: .25rem!important;
+//}
+//
+//.label-group {
+// display: flex;
+// gap: .75rem;
+// align-items: center;
+// flex-wrap: wrap;
+//}
+//.label-group .icon {
+// margin-right: .5rem;
+//}
+//.item-grid.table {
+// display: none;
+//}
+//
+//.item-grid summary {
+// padding: 0;
+// gap: 0;
+//}
+//.item-grid .item-actions {
+// position: absolute;
+// top: .25rem;
+// right: .25rem;
+// gap: .25rem;
+// display: flex;
+//}
+//.item-grid .item-actions button {
+// width: 2em;
+// height: 2em;
+// border-radius: var(--radius);
+// background-color: rgba(var(--base-rgb),var(--op-3));
+// display: flex;
+// justify-content: center;
+// align-items: center;
+//}
+//.item-grid .item-actions button:focus,
+//.item-grid .item-actions button:hover {
+// background-color: rgba(var(--base-rgb),var(--op-6));
+// color: var(--action-0);
+//}
+//
+//
+//
+//@media (max-width: 768px){
+// .replace {
+// margin-top: 4rem!important;
+// }
+// .list-header {
+// flex-wrap: wrap;
+// }
+// .list-card h3 {
+// font-size: var(--txt-medium);
+// }
+// .item-grid.list-view .item {
+// align-items: center;
+// }
+// .item-grid.list-view img {
+// width: 80px;
+// height: 80px;
+// aspect-ratio: 1;
+// object-fit: cover;
+// }
+//}
+//
+//@media (min-width: 768px){
+// .item-grid:not(.list-view) {
+// grid-template-columns: repeat(3, 1fr);
+// }
+//}
+//
+//.term-divider {
+// position: relative;
+// text-align: center;
+// margin: 1rem 0;
+// border-bottom: 1px solid var(--base-200);
+//}
+//
+//.term-divider span {
+// background: var(--base);
+// padding: 0 1rem;
+// color: var(--contrast);
+// font-size: .9rem;
+// position: relative;
+// top: .5em;
+//}
+//
+//.common-term {
+// background: var(--base-50);
+// border-radius: var(--radius);
+//}
+//
+//.loading-indicator {
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// gap: .5rem;
+// padding: 1rem;
+// color: var(--contrast-100);
+// font-size: .9rem;
+//}
+//
+//.loading-indicator svg {
+// animation: spin 1s linear infinite;
+//}
+//
+//.pagination-info {
+// text-align: center;
+// padding: .5rem;
+// font-size: .9rem;
+// color: var(--contrast-100);
+// border-top: 1px solid var(--base-100);
+//}
+//
+//@keyframes spin {
+// from { transform: rotate(0deg); }
+// to { transform: rotate(360deg); }
+//}
+//
+//
+//.term-breadcrumb {
+// margin-bottom: 1rem;
+// padding: .5rem;
+// background: var(--base-50);
+// border-radius: 4px;
+//}
+//
+//.back-to-parent {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// border: none;
+// background: none;
+// color: var(--contrast);
+// cursor: pointer;
+// padding: .5rem;
+// border-radius: 4px;
+// font-size: var(--txt-x-small);
+//}
+//
+//.back-to-parent:hover {
+// background: var(--base-100);
+//}
+//
+//.term-row {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// width: 100%;
+// padding: .25rem 0;
+//}
+//
+//.toggle-children {
+// border: none;
+// background: none;
+// padding: .25rem;
+// cursor: pointer;
+// color: var(--contrast);
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// margin-left: auto;
+// border-radius: 4px;
+//}
+//
+//.toggle-children:hover {
+// background: var(--base-50);
+//}
+//
+//.loading-indicator {
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// width: 24px;
+// height: 24px;
+//}
+//
+//.loading-indicator .loading {
+// width: 16px;
+// height: 16px;
+// border: 2px solid var(--base-100);
+// border-top-color: var(--contrast);
+// border-radius: 50%;
+// animation: spin 1s linear infinite;
+//}
+//
+//@keyframes spin {
+// to { transform: rotate(360deg); }
+//}
+//
+//.term-breadcrumb {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// margin-bottom: 1rem;
+// padding: .5rem;
+// background: var(--base-50);
+// border-radius: 4px;
+//}
+//
+//.term-breadcrumb .path {
+// display: flex;
+// align-items: center;
+// gap: .25rem;
+// flex-wrap: wrap;
+//}
+//
+//.term-breadcrumb button {
+// border: none;
+// background: none;
+// padding: .25rem .5rem;
+// border-radius: 4px;
+// cursor: pointer;
+// color: var(--contrast);
+// font-size: var(--txt-x-small);
+//}
+//
+//.term-breadcrumb button:hover {
+// background: var(--base-100);
+//}
+//
+//.path-separator {
+// color: var(--contrast-50);
+//}
+//
+//.path-level {
+// white-space: nowrap;
+//}
+//
+//.create-new-term {
+// margin-top: 2rem;
+// padding-top: 1rem;
+// border-top: 1px solid var(--base-100);
+//}
+//.create-new-term button {
+// width: 100%;
+//}
+//
+//.suggestion-prompt {
+// font-size: var(--txt-x-small);
+// color: var(--contrast-50);
+// margin-bottom: 1rem;
+//}
+//
+//.create-term-form {
+// display: flex;
+// flex-direction: column;
+// gap: .5rem;
+//}
+//
+//.form-row {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+//}
+//
+//.name-row {
+// position: relative;
+//}
+//
+//.name-row input {
+// width: 100%!important;
+// padding: .5rem;
+// border: 2px solid var(--base-100);
+// border-radius: 4px;
+// background: var(--base);
+// color: var(--contrast);
+//}
+//
+//.name-row input:focus {
+// border-color: var(--action-0);
+// outline: none;
+//}
+//
+//.parent-row {
+// font-size: var(--txt-x-small);
+//}
+//
+//.parent-row label {
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// cursor: pointer;
+//}
+//
+//.create-term-form button {
+// display: inline-flex;
+// align-items: center;
+// gap: .5rem;
+// padding: .5rem 1rem;
+// border: none;
+// border-radius: 4px;
+// background: var(--action-0);
+// color: var(--base);
+// cursor: pointer;
+// font-size: var(--txt-x-small);
+// transition: all .2s ease;
+//}
+//
+//.create-term-form button:hover {
+// background: var(--action-50);
+//}
+//
+//.create-term-form button:disabled {
+// opacity: .5;
+// cursor: not-allowed;
+//}
+//
+//.create-term-form button svg {
+// width: 16px;
+// height: 16px;
+//}
+//
+///** Search bar and new term functionality **/
+//.search-bar {
+// position: relative;
+// display: flex;
+// gap: .5rem;
+// align-items: center;
+// z-index: 2;
+//}
+//
+//.search-bar input {
+// flex: 1;
+// /* Make room for button */
+// padding: .5rem 2.5rem .5rem .5rem;
+// border: 2px solid #ddd;
+// border-radius: 4px;
+// font-size: 1rem;
+//}
+//
+//.new-term-toggle {
+// margin: 2rem 0 1rem;
+// width: 100%;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+//}
+//
+///* Peek effect on search bar hover */
+//.search-bar:hover .new-term-toggle {
+// opacity: 1;
+// transform: translateX(-.25rem);
+//}
+//
+///* Form styling */
+//.create-term-form {
+// position: relative;
+// z-index: 1;
+// background: var(--base);
+// border: 2px solid var(--action-0);
+// border-radius: 4px;
+// padding: 1rem;
+// margin-top: -2px;
+//
+// /* Animation */
+// transform-origin: top;
+// transition: all .2s var(--trans-fn);
+//}
+//
+//.create-term-form:not([hidden]) {
+// animation: slideDown .2s var(--trans-fn);
+//}
+//
+//.create-term-form[hidden] {
+// display: block !important; /* Override hidden */
+// transform: scaleY(0);
+// opacity: 0;
+// pointer-events: none;
+//}
+//
+//.form-row {
+// display: flex;
+// gap: .5rem;
+// align-items: center;
+//}
+//
+//.form-row + .form-row {
+// margin-top: .5rem;
+//}
+//
+//@keyframes slideDown {
+// from {
+// transform: scaleY(0);
+// opacity: 0;
+// }
+// to {
+// transform: scaleY(1);
+// opacity: 1;
+// }
+//}
+//
+//.news.item {
+// padding: 1rem!important;
+//}
+//
+//.dashboard .checkbox-options label {
+// font-weight: normal;
+//}
+//
+//.description.space {
+// margin-bottom: 4rem;
+//}
+//
+//.invited-artist {
+// display: flex;
+// align-items: center;
+// margin: 1rem 0;
+//}
+//.invited-artist input[type=text],
+//.invited-artist input[type=email] {
+// width: calc(100% - .5rem);
+//}
+//.invited-artist label {
+// margin: 0;
+// font-weight: normal;
+// white-space:nowrap;
+//}
+//.invited-artist button {
+// height: fit-content;
+// margin-right: .5rem;
+//}
+//.actions {
+// display: flex;
+// gap: 1rem;
+// justify-content: flex-end;
+//}
+//
+//.actions .send-invites {
+// background-color: var(--action-50);
+//}
+//.actions .send-invites:hover,
+//.actions .send-invites:focus {
+// background-color: var(--base);
+// border-color: var(--action-50);
+//}
+//
+//.dashboard .queue-status-panel {
+// bottom: calc(var(--btn) + 1rem);
+//}
+//.dashboard .queue-status-toggle {
+// bottom: 0;
+//}
+//
+//.items-list table th {
+// padding: 0 .75rem;
+// text-transform: none!important;
+//}
+//.items-list table.empty tbody {
+// height: 40vh;
+//}
+//
+//table textarea,
+//table input[type=text],
+//table input[type=url],
+//table input[type=number],
+//table input[type=email] {
+// min-width: 40vw;
+//}
+//
+//.empty tfoot {
+// display: none;
+//}
+//
+//p.hint {
+// margin: 0 0 .5rem 0;
+// font-size: var(--txt-x-small);
+// font-style: italic;
+//}
+//.item-grid + .hint {
+// text-align: center;
+// margin-top: 2rem;
+//}
+//
+//.image.field[data-type=single]:has(.upload-item) .file-upload-container {
+// display: none;
+//}
+//.image.field[data-type=single] .upload-item {
+// grid-column: 1 / -1;
+//}
+///** UPLOADER **/
+//.upload-item {
+// position: relative;
+// border: 1px solid var(--base-200);
+// border-radius: 8px;
+// background: var(--base-50);
+// transition: all .3s ease;
+//}
+//.upload-item:hover {
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// transform: translateY(-2px);
+//}
+//.upload-item[data-status=processing] {
+// border-color: var(--action-200);
+// background: var(--action-100);
+//}
+//.upload-item[data-status=cached] {
+// border-color: var(--secondary-0);
+// background: var(--secondary-200);
+//}
+//.upload-item[data-status=uploading] {
+// border-color: var(--contrast);
+// background-color: var(--base-50);
+//}
+//.upload-item .preview {
+// position: relative;
+// aspect-ratio: 1;
+//}
+//.upload-item .status {
+// position: absolute;
+// bottom: .25rem;
+// right: .25rem;
+// background-color: rgba(var(--base-rgb),var(--op-3));
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// border-radius: 50%;
+//}
+//.upload-item img {
+// width: 100%;
+// height: 100%;
+// object-fit: cover;
+// transition: transform .3s ease;
+//}
+//.upload-item:hover img {
+// transform: scale(1.05);
+//}
+//
+//.upload-item .overlay {
+// position: absolute;
+// top: 0;
+// left: 0;
+// right: 0;
+// bottom: 0;
+// background: rgba(var(--base-rgb),var(--op-6));
+// display: flex;
+// flex-direction: column;
+// justify-content: space-between;
+// padding: .5rem;
+// opacity: 0;
+// transition: opacity .3s ease;
+//}
+//.upload-item:hover .overlay,
+//.upload-item[data-status=processing] .overlay,
+//.upload-item[data-status=uploading] .overlay {
+// opacity: 1;
+//}
+//
+//.upload-item .item-actions {
+// position: relative;
+//}
+//
+//.submit-uploads {
+// position: fixed;
+// bottom: calc(var(--btn) + 1rem);
+// right: 1rem;
+// background-color: var(--base);
+// height: var(--btn);
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+//}
+///*** UPLOADER GROUPS ***/
+//.group-display {
+// display: grid;
+// grid-template-columns: 1fr 250px;
+// gap: 2rem;
+//}
+//.preview-actions {
+// position: sticky;
+// padding: .5rem;
+// top: calc(var(--btn) + .25rem);
+// left: 0;
+// background-color: var(--base-50);
+// z-index: 5;
+//}
+//.preview-actions .selected {
+// display: flex;
+// justify-content: space-between;
+// padding-right: 2rem;
+// align-items: center;
+//}
+//.preview-actions .field {
+// margin: 0;
+//}
+//.preview-actions .field label {
+// margin-bottom: 0;
+//}
+//.selection-actions {
+// display: flex;
+// justify-content: space-between;
+// padding: 1rem 2rem;
+//}
+//[data-status=completed] .progress {
+// transform: scaleY(0);
+// transform-origin: top;
+//}
+//.preview-grid {
+// display: grid;
+// grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+// gap: 1rem;
+// margin-top: 1rem;
+// padding: .5rem;
+// background: var(--base);
+// border: 1px solid var(--contrast-200);
+// border-radius: 8px;
+// min-height: 60px;
+//}
+//.preview-grid:empty {
+// display: none;
+//}
+//
+//
+//.upload-item .progress {
+// position: absolute;
+// top: 2px;
+// right: 2px;
+// left: 2px;
+//}
+//.upload-item .actions {
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+// position: absolute;
+// top: 0;
+// left: 0;
+// right: 0;
+//}
+//.upload-item .actions > label {
+// margin: 0 1rem;
+// padding-left: 0!important;
+//}
+//.upload-item input[type=text],
+//.upload-item textarea,
+//.group-fields input[type=text],
+//.group-fields textarea {
+// width: calc(100% - 1rem);
+//}
+///*.upload-item .actions > label {*/
+///* !*position: absolute;*!*/
+///* padding: 1rem!important;*/
+///* margin: 0;*/
+///* !*top: .5rem;*!*/
+///* !*left: 0;*!*/
+///*}*/
+///* .upload-item .actions > label::before {*/
+///* left: calc(50% - 9px)!important;*/
+///* top: calc(50% - 9px)!important;*/
+///* transform: none!important;*/
+///* }*/
+///* .upload-item .actions > label::after {*/
+///* left: 1.25rem!important;*/
+///* top: 1rem!important;*/
+///* }*/
+//.item-grid .upload-item summary {
+// display: flex;
+// align-items: center;
+// font-size: var(--txt-x-small);
+// gap: .5rem;
+// text-transform: uppercase;
+//}
+//.upload-item:has([type=checkbox]:checked) .preview {
+// padding: 1rem;
+// background-color: var(--secondary-200);
+//}
+//.upload-item:has([open]) {
+// grid-column: 1 / -1;
+//}
+//.upload-item summary .icon {
+// --w: 1.1em;
+//}
+//
+//[draggable="true"] {
+// cursor: grab;
+//}
+//[draggable="true"]:active {
+// cursor: grabbing;
+//}
+//
+//.dragging {
+// opacity: .5;
+// transform: rotate(5deg) scale(.95);
+// transition: all .2s ease;
+//}
+//
+//.dragover:not(.item-grid.groups) {
+// background-color: var(--overlay-action-medium);
+// border: 2px dashed var(--action-0);
+//}
+//
+//@keyframes dragHover {
+// 0% { transform: scale(1); }
+// 50% { transform: scale(1.02); }
+// 100% { transform: scale(1); }
+//}
+//
+//.drag-hover {
+// animation: dragHover .6s ease-in-out infinite;
+//}
+//
+///** GROUP **/
+//.empty-group {
+// padding: 20px;
+// text-align: center;
+// grid-column: 1 / -1;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// border: 2px dashed var(--action-200);
+// border-radius: var(--radius);
+// margin: 10px 0;
+// cursor: pointer;
+// transition: all .2s ease;
+// aspect-ratio: 16/9;
+//}
+//.item-grid.groups {
+// grid-template-columns: 1fr;
+//}
+//
+//.item-grid.group .item-grid.group {
+// min-height: 40px;
+// display: grid;
+// grid-template-columns: repeat(3, 1fr);
+// gap: .5rem;
+// border: 2px dashed var(--action-200);
+// border-radius: var(--radius);
+// margin: 10px 0;
+// cursor: pointer;
+// transition: all .2s ease;
+// aspect-ratio: 1;
+// text-align: center;
+//}
+//.empty-group:hover,
+//.empty-group.dragover,
+//.item-grid.group .item-grid.group:hover,
+//.item-grid.group .item-grid.group.dragover {
+// border-color: var(--action-0);
+// background-color: var(--overlay-action-light);
+// color: var(--action-50);
+//}
+//
+//.upload-group {
+// background-color: var(--base-100);
+// border-radius: var(--radius);
+// border: 1px solid var(--contrast-200);
+//}
+//.group-actions {
+// width: 100%;
+// gap: .5rem;
+//}
+//.group-actions button {
+// width: 100%;
+//}
+//
+//body:has(.selection-actions[hidden]) button.create-group,
+//body:has(.selection-actions[hidden]) button.add-selection-to-group {
+// display: none;
+//}
+//
+///** RESTORE FROM CACHE **/
+//.restore-notification {
+// border-radius: var(--radius);
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// padding: 1rem;
+// background: var(--base-200);
+// border: 1px solid var(--contrast-200);
+// border-top-width: 4px;
+// border-bottom-width: 4px;
+//}
+//.restore-content {
+// display: flex;
+// align-items: center;
+//}
+//.restore-actions .selection-controls {
+// display: flex;
+// gap: .5rem;
+//}
+//
+//.restore-actions button {
+// white-space: nowrap;
+//}
+//.restore-actions .action-buttons {
+// margin-top: 1rem;
+// display: flex;
+// flex-direction: column;
+// gap: .5rem;
+//}
+//.restore-actions .action-buttons button {
+// width: 100%;
+//}
+//
+//label.restore-item {
+// border-radius: .5rem;
+// padding: 0;
+// background-color: transparent;
+// filter: grayscale(50);
+// opacity: .8;
+// transition: padding var(--trans-base);
+// transition-property: padding, background-color;
+// cursor: pointer;
+//}
+//label.restore-item:has(input:checked) {
+// filter: grayscale(0);
+// opacity: 1;
+// padding: 1rem;
+// background-color: var(--secondary-200);
+//}
+//
+//.upload-item .featured + label {
+// width: 2em;
+// height: 2em;
+// border-radius: var(--radius);
+// background-color: rgba(var(--base-rgb),var(--op-3));
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// padding: 0!important;
+// border: none;
+//}
+//
+//input.featured + label .icon {
+// --w: 1.25em;
+//}
+//input.featured + label .icon + .icon {
+// position: absolute;
+//}
+//input.featured + label .star-fill,
+//input.featured:checked + label .star {
+// transform: scale(0);
+//}
+//input.featured:checked + label .star-fill,
+//input.featured + label .star {
+// transform: scale(1);
+//}
+///**TODO: Double check needed **/
+///*!* Centralized Upload Manager CSS *!*/
+//
+///*!* Global Upload Status Bar *!*/
+///*.all-uploads {*/
+///* position: fixed;*/
+///* top: 0;*/
+///* left: 0;*/
+///* right: 0;*/
+///* background: var(--surface);*/
+///* border-bottom: 1px solid var(--border);*/
+///* padding: .75rem 1rem;*/
+///* z-index: 1000;*/
+///* box-shadow: 0 2px 8px rgba(0, 0, 0, .1);*/
+///* backdrop-filter: blur(10px);*/
+///* display: none; !* Hidden by default *!*/
+///*}*/
+//
+///*.upload-summary {*/
+///* display: flex;*/
+///* justify-content: space-between;*/
+///* align-items: center;*/
+///* margin-bottom: .5rem;*/
+///*}*/
+//
+///*.all-uploads .active {*/
+///* font-weight: 500;*/
+///* color: var(--contrast);*/
+///* font-size: .9rem;*/
+///*}*/
+//
+///*.upload-summary button {*/
+///* padding: .5rem 1rem;*/
+///* border: 1px solid var(--border);*/
+///* border-radius: 4px;*/
+///* background: var(--surface);*/
+///* color: var(--contrast);*/
+///* cursor: pointer;*/
+///* transition: all .3s ease;*/
+///* font-size: .875rem;*/
+///* margin-left: .5rem;*/
+///*}*/
+//
+///*.upload-summary button:hover {*/
+///* background: var(--action-0);*/
+///* color: var(--action-contrast);*/
+///* border-color: var(--action-0);*/
+///*}*/
+//
+///*!* Upload Drop Zones *!*/
+///*.file-upload-container {*/
+///* position: relative;*/
+///* padding: .25rem;*/
+///* transition: border-color var(--trans-base),*/
+///* background-color var(--trans-base),*/
+///* padding var(--trans-base);*/
+///*}*/
+//
+///*.file-upload-container.dragover {*/
+///* background-color: var(--action-rgb-subtle);*/
+///* border-color: var(--action-0);*/
+///* padding: .75rem;*/
+///*}*/
+///* .dragover .file-upload-wrapper {*/
+///* transform: scale(1.02);*/
+///* }*/
+//
+//
+///*!* Upload Actions *!*/
+///*.upload .actions {*/
+///* display: flex;*/
+///* gap: .25rem;*/
+///* justify-content: flex-end;*/
+///*}*/
+//
+///*.upload .actions button {*/
+///* background: rgba(0, 0, 0, .7);*/
+///* color: white;*/
+///* border: none;*/
+///* padding: .25rem;*/
+///* border-radius: 4px;*/
+///* cursor: pointer;*/
+///* transition: background .3s ease;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///*}*/
+//
+///*.upload .actions button:hover {*/
+///* background: rgba(0, 0, 0, .9);*/
+///*}*/
+//
+///*.upload .actions button svg {*/
+///* width: 16px;*/
+///* height: 16px;*/
+///*}*/
+//
+///*!* Status Indicator *!*/
+///*.status {*/
+///* position: absolute;*/
+///* top: .5rem;*/
+///* right: .5rem;*/
+///* width: 24px;*/
+///* height: 24px;*/
+///* border-radius: 50%;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* font-size: .8rem;*/
+///* font-weight: bold;*/
+///* z-index: 2;*/
+///* box-shadow: 0 2px 4px rgba(0, 0, 0, .2);*/
+///*}*/
+//
+///*.status.processing {*/
+///* background: var(--warning);*/
+///* color: var(--warning-contrast);*/
+///*}*/
+//
+///*.status.processed {*/
+///* background: var(--info);*/
+///* color: var(--info-contrast);*/
+///*}*/
+//
+///*.status.cached {*/
+///* background: var(--info);*/
+///* color: var(--info-contrast);*/
+///*}*/
+//
+///*.status.uploading {*/
+///* background: var(--action-0);*/
+///* color: var(--action-contrast);*/
+///*}*/
+//
+///*.status.uploaded {*/
+///* background: var(--success);*/
+///* color: var(--success-contrast);*/
+///*}*/
+//
+///*.status.error {*/
+///* background: var(--error);*/
+///* color: var(--error-contrast);*/
+///*}*/
+//
+///*!* Upload Metadata *!*/
+///*.upload .metadata {*/
+///* padding: 1rem;*/
+///* border-top: 1px solid var(--border);*/
+///* background: var(--surface);*/
+///*}*/
+//
+///*.upload .metadata .metadata-field {*/
+///* margin-bottom: .75rem;*/
+///*}*/
+//
+///*.upload .metadata .metadata-field:last-child {*/
+///* margin-bottom: 0;*/
+///*}*/
+//
+///*.upload .metadata label {*/
+///* display: block;*/
+///* margin-bottom: .25rem;*/
+///* font-weight: 500;*/
+///* font-size: .9rem;*/
+///* color: var(--contrast);*/
+///*}*/
+//
+///*.upload .metadata input,*/
+///*.upload .metadata textarea {*/
+///* width: 100%;*/
+///* padding: .5rem;*/
+///* border: 1px solid var(--border);*/
+///* border-radius: 4px;*/
+///* font-size: .9rem;*/
+///* transition: border-color .3s ease, box-shadow .3s ease;*/
+///*}*/
+//
+///*.upload .metadata input:focus,*/
+///*.upload .metadata textarea:focus {*/
+///* outline: none;*/
+///* border-color: var(--action-0);*/
+///* box-shadow: 0 0 0 2px var(--action-rgb-subtle);*/
+///*}*/
+//
+///*.upload .metadata textarea {*/
+///* resize: vertical;*/
+///* min-height: 2.5rem;*/
+///*}*/
+//
+///*!* Field Type Specific Styles *!*/
+//
+///*!* Single Image Field *!*/
+///*.upload.field[data-upload-type="single"] .upload-preview-grid {*/
+///* grid-template-columns: 1fr;*/
+///* max-width: 400px;*/
+///*}*/
+//
+///*!* Gallery Field *!*/
+///*.upload.field[data-upload-type="gallery"] .upload-preview-grid {*/
+///* grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));*/
+///*}*/
+//
+///*!* Avatar Field *!*/
+///*.upload.field[data-upload-type="avatar"] .upload-preview-grid {*/
+///* grid-template-columns: 1fr;*/
+///* max-width: 200px;*/
+///*}*/
+//
+///*.upload.field[data-upload-type="avatar"] .upload-preview {*/
+///* aspect-ratio: 1;*/
+///* border-radius: 50%;*/
+///* overflow: hidden;*/
+///*}*/
+//
+///*!* Spinner Animation *!*/
+///*.spinner {*/
+///* width: 12px;*/
+///* height: 12px;*/
+///* border: 2px solid transparent;*/
+///* border-top: 2px solid currentColor;*/
+///* border-radius: 50%;*/
+///* animation: spin 1s linear infinite;*/
+///*}*/
+//
+///*@keyframes spin {*/
+///* 0% { transform: rotate(0deg); }*/
+///* 100% { transform: rotate(360deg); }*/
+///*}*/
+//
+///*@keyframes shimmer {*/
+///* 0% { transform: translateX(-100%); }*/
+///* 100% { transform: translateX(100%); }*/
+///*}*/
+//
+///*!* Responsive Design *!*/
+///*@media (max-width: 768px) {*/
+///* .all-uploads {*/
+///* position: relative;*/
+///* top: auto;*/
+///* left: auto;*/
+///* right: auto;*/
+///* }*/
+//
+///* .upload-summary {*/
+///* flex-direction: column;*/
+///* gap: .5rem;*/
+///* align-items: stretch;*/
+///* }*/
+//
+///* .upload-summary button {*/
+///* margin-left: 0;*/
+///* width: 100%;*/
+///* }*/
+//
+///* .upload-preview-grid {*/
+///* grid-template-columns: 1fr;*/
+///* gap: .75rem;*/
+///* }*/
+//
+///* .upload.field[data-upload-type="gallery"] .upload-preview-grid {*/
+///* grid-template-columns: repeat(2, 1fr);*/
+///* }*/
+///*}*/
+//
+///*!* Dark Mode Support *!*/
+///*@media (prefers-color-scheme: dark) {*/
+///* .upload-overlay {*/
+///* background: linear-gradient(to bottom, rgba(0,0,0,.3), rgba(0,0,0,.6));*/
+///* }*/
+//
+///*}*/
+//
+///*!* High Contrast Mode *!*/
+///*@media (prefers-contrast: high) {*/
+///* .upload-item {*/
+///* border-width: 2px;*/
+///* }*/
+//
+///* .status {*/
+///* border: 2px solid currentColor;*/
+///* }*/
+//
+///*}*/
+//
+///*!* Reduced Motion *!*/
+///*@media (prefers-reduced-motion: reduce) {*/
+///* .upload-item:hover {*/
+///* transform: none;*/
+///* }*/
+//
+///* .upload-item:hover .upload-preview img {*/
+///* transform: none;*/
+///* }*/
+//
+///* .spinner,*/
+///* .shimmer {*/
+///* animation: none;*/
+///* }*/
+///*}*/
+//
+///*!* Focus Management *!*/
+///*.upload-item:focus-within {*/
+///* outline: 2px solid var(--action-0);*/
+///* outline-offset: 2px;*/
+///*}*/
+//
+///*.upload .actions button:focus {*/
+///* outline: 2px solid var(--action-0);*/
+///* outline-offset: 2px;*/
+///*}*/
+//
+///*!* Loading States *!*/
+///*.upload.field.processing {*/
+///* pointer-events: none;*/
+///* opacity: .7;*/
+///*}*/
+//
+///*.upload.field.processing::after {*/
+///* content: '';*/
+///* position: absolute;*/
+///* top: 0;*/
+///* left: 0;*/
+///* right: 0;*/
+///* bottom: 0;*/
+///* background: rgba(255, 255, 255, .5);*/
+///* backdrop-filter: blur(2px);*/
+///* z-index: 10;*/
+///*}*/
+//
+///*!* Field Capacity Indicators *!*/
+///*.upload.field[data-at-capacity="true"] .file-upload-container {*/
+///* opacity: .5;*/
+///* pointer-events: none;*/
+///*}*/
+//
+///*.upload.field[data-at-capacity="true"]::before {*/
+///* content: 'Maximum files reached';*/
+///* position: absolute;*/
+///* top: 50%;*/
+///* left: 50%;*/
+///* transform: translate(-50%, -50%);*/
+///* background: var(--warning);*/
+///* color: var(--warning-contrast);*/
+///* padding: .5rem 1rem;*/
+///* border-radius: 4px;*/
+///* font-size: .875rem;*/
+///* z-index: 5;*/
+///*}*/
+//
+///*!* Integration with existing form styles *!*/
+///*.upload.field {*/
+///* position: relative;*/
+///*}*/
+//
+///*.upload.field .upload-preview-grid {*/
+///* margin-top: .5rem;*/
+///*}*/
+//
+///*!* Ensure global status doesn't interfere with fixed elements *!*/
+///*body.has-global-upload-status {*/
+///* padding-top: 80px;*/
+///*}*/
+//
+///*!* Animation for upload completion *!*/
+///*@keyframes uploadSuccess {*/
+///* 0% { transform: scale(1); }*/
+///* 50% { transform: scale(1.05); }*/
+///* 100% { transform: scale(1); }*/
+///*}*/
+//
+///*.upload-item[data-status="uploaded"] {*/
+///* animation: uploadSuccess .6s ease-out;*/
+///*}*/
+///*!* Batch Processing Progress Bar *!*/
+///*.batch-progress {*/
+///* margin: 1rem 0;*/
+///* padding: 1rem;*/
+///* background: var(--color-background-secondary, #f8f9fa);*/
+///* border: 1px solid var(--color-border, #e1e5e9);*/
+///* border-radius: 8px;*/
+///* transition: opacity .3s ease;*/
+///*}*/
+//
+///*.batch-progress.completed {*/
+///* background: var(--color-success-light, #d4edda);*/
+///* border-color: var(--color-success, #28a745);*/
+///*}*/
+//
+///*.progress-info {*/
+///* display: flex;*/
+///* justify-content: space-between;*/
+///* align-items: center;*/
+///* margin-bottom: .5rem;*/
+///* font-size: .9rem;*/
+///*}*/
+//
+///*.progress-message {*/
+///* color: var(--color-text-secondary, #6c757d);*/
+///* font-weight: 500;*/
+///*}*/
+//
+///*.progress-count {*/
+///* color: var(--color-text-primary, #212529);*/
+///* font-weight: 600;*/
+///* font-variant-numeric: tabular-nums;*/
+///*}*/
+//
+///*.progress .bar {*/
+///* width: 100%;*/
+///* height: 8px;*/
+///* background: var(--color-background-tertiary, #e9ecef);*/
+///* border-radius: 4px;*/
+///* overflow: hidden;*/
+///*}*/
+//
+///*.progress .fill {*/
+///* height: 100%;*/
+///* background: linear-gradient(90deg,*/
+///* var(--color-primary, #007cba) 0%,*/
+///* var(--color-primary-light, #0096dd) 100%*/
+///* );*/
+///* border-radius: 4px;*/
+///* transition: width .3s ease;*/
+///* position: relative;*/
+///*}*/
+//
+///*!* Animated progress indicator *!*/
+///*.progress .fill::after {*/
+///* content: '';*/
+///* position: absolute;*/
+///* top: 0;*/
+///* left: 0;*/
+///* right: 0;*/
+///* bottom: 0;*/
+///* background: linear-gradient(*/
+///* 90deg,*/
+///* transparent 0%,*/
+///* rgba(255,255,255,.3) 50%,*/
+///* transparent 100%*/
+///* );*/
+///* animation: progress-shine 2s ease-in-out infinite;*/
+///*}*/
+//
+///*@keyframes progress-shine {*/
+///* 0% { transform: translateX(-100%); }*/
+///* 100% { transform: translateX(100%); }*/
+///*}*/
+//
+///*!* Completed state styling *!*/
+///*.progress.completed .fill {*/
+///* background: linear-gradient(90deg,*/
+///* var(--color-success, #28a745) 0%,*/
+///* var(--color-success-light, #34ce57) 100%*/
+///* );*/
+///*}*/
+//
+///*.progress.completed .progress-message {*/
+///* color: var(--color-success-dark, #155724);*/
+///*}*/
+//
+///*!* Dark mode support *!*/
+///*@media (prefers-color-scheme: dark) {*/
+///* .progress {*/
+///* background: var(--color-background-secondary-dark, #2d3436);*/
+///* border-color: var(--color-border-dark, #636e72);*/
+///* }*/
+//
+///* .progress .bar {*/
+///* background: var(--color-background-tertiary-dark, #636e72);*/
+///* }*/
+///*}*/
+//
+///*!* ✅ Grouping System CSS *!*/
+//
+///*!* Upload field container with grouping support *!*/
+///*.upload.field.groupable {*/
+///* display: grid;*/
+///* grid-template-columns: 1fr 300px;*/
+///* gap: 20px;*/
+///* min-height: 400px;*/
+///*}*/
+//
+///*.upload.field.groupable .upload-main {*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///*}*/
+//
+///*.upload.field.groupable .upload-sidebar {*/
+///* border-left: 2px solid #e0e0e0;*/
+///* padding-left: 20px;*/
+///*}*/
+//
+///*!* Preview grid for groupable uploads *!*/
+///*.upload-preview-grid.groupable {*/
+///* min-height: 200px;*/
+///* border: 2px dashed #ddd;*/
+///* border-radius: 8px;*/
+///* padding: 15px;*/
+///* background: #fafafa;*/
+///*}*/
+//
+///*.upload-preview-grid.groupable:empty::after {*/
+///* content: "Drop images here or click to upload";*/
+///* display: block;*/
+///* text-align: center;*/
+///* color: #666;*/
+///* font-style: italic;*/
+///* padding: 40px 20px;*/
+///*}*/
+//
+///*!* Upload items with drag support *!*/
+///*.upload-item[draggable] {*/
+///* cursor: grab;*/
+///* transition: transform .2s ease, box-shadow .2s ease;*/
+///*}*/
+//
+///*.upload-item[draggable]:hover {*/
+///* transform: translateY(-2px);*/
+///* box-shadow: 0 4px 8px rgba(0,0,0,.1);*/
+///*}*/
+//
+///*.upload-item.dragging {*/
+///* opacity: .5;*/
+///* transform: rotate(5deg);*/
+///* cursor: grabbing;*/
+///*}*/
+//
+///*!* Groups container *!*/
+///*.groups-container {*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///* gap: 15px;*/
+///* max-height: 600px;*/
+///* overflow-y: auto;*/
+///* padding: 10px;*/
+///* background: #f9f9f9;*/
+///* border-radius: 8px;*/
+///*}*/
+//
+///*.groups-container .new-group-section {*/
+///* text-align: center;*/
+///* padding: 20px;*/
+///* border: 2px dashed #ccc;*/
+///* border-radius: 8px;*/
+///* background: white;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.groups-container .new-group-section:hover {*/
+///* border-color: #2196F3;*/
+///* background: #f0f8ff;*/
+///*}*/
+//
+///*.new-group-btn {*/
+///* background: #2196F3;*/
+///* color: white;*/
+///* border: none;*/
+///* padding: 10px 20px;*/
+///* border-radius: 6px;*/
+///* cursor: pointer;*/
+///* font-size: 14px;*/
+///* display: inline-flex;*/
+///* align-items: center;*/
+///* gap: 8px;*/
+///*}*/
+//
+///*.new-group-btn:hover {*/
+///* background: #1976D2;*/
+///*}*/
+//
+///*!* Individual groups *!*/
+///*.upload-group {*/
+///* background: white;*/
+///* border: 1px solid #e0e0e0;*/
+///* border-radius: 8px;*/
+///* padding: 15px;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.upload-group.drag-hover {*/
+///* border-color: #2196F3;*/
+///* background: #f0f8ff;*/
+///* box-shadow: 0 2px 8px rgba(33, 150, 243, .2);*/
+///*}*/
+//
+///*.group-header {*/
+///* display: flex;*/
+///* justify-content: space-between;*/
+///* align-items: center;*/
+///* margin-bottom: 15px;*/
+///* padding-bottom: 10px;*/
+///* border-bottom: 1px solid #f0f0f0;*/
+///*}*/
+//
+///*.group-info {*/
+///* flex: 1;*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///* gap: 5px;*/
+///*}*/
+//
+///*.group-name {*/
+///* border: 1px solid #ddd;*/
+///* border-radius: 4px;*/
+///* padding: 6px 10px;*/
+///* font-size: 14px;*/
+///* font-weight: 500;*/
+///* background: white;*/
+///* transition: border-color .2s ease;*/
+///*}*/
+//
+///*.group-name:focus {*/
+///* outline: none;*/
+///* border-color: #2196F3;*/
+///* box-shadow: 0 0 0 2px rgba(33, 150, 243, .2);*/
+///*}*/
+//
+///*.group-name:empty::before {*/
+///* content: "Enter group name...";*/
+///* color: #999;*/
+///* font-style: italic;*/
+///*}*/
+//
+///*.group-count {*/
+///* font-size: 12px;*/
+///* color: #666;*/
+///* font-weight: normal;*/
+///*}*/
+//
+///*.group-actions {*/
+///* display: flex;*/
+///* gap: 8px;*/
+///*}*/
+//
+///*.delete-group {*/
+///* background: #f44336;*/
+///* color: white;*/
+///* border: none;*/
+///* padding: 6px 8px;*/
+///* border-radius: 4px;*/
+///* cursor: pointer;*/
+///* font-size: 12px;*/
+///* transition: background .2s ease;*/
+///*}*/
+//
+///*.delete-group:hover {*/
+///* background: #d32f2f;*/
+///*}*/
+//
+///*!* Group drop zone *!*/
+///*.group-drop-zone {*/
+///* min-height: 80px;*/
+///* border: 2px dashed #ccc;*/
+///* border-radius: 6px;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* margin-bottom: 15px;*/
+///* transition: all .2s ease;*/
+///* background: #fafafa;*/
+///*}*/
+//
+///*.group-drop-zone p {*/
+///* color: #666;*/
+///* font-style: italic;*/
+///* margin: 0;*/
+///* font-size: 14px;*/
+///*}*/
+//
+///*.group-drop-zone.drag-hover {*/
+///* border-color: #4CAF50;*/
+///* background: #f1f8e9;*/
+///*}*/
+//
+///*!* Group items *!*/
+///*.group-items {*/
+///* display: grid;*/
+///* grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));*/
+///* gap: 10px;*/
+///* margin-bottom: 15px;*/
+///*}*/
+//
+///*.group-item {*/
+///* position: relative;*/
+///* background: white;*/
+///* border: 1px solid #e0e0e0;*/
+///* border-radius: 6px;*/
+///* overflow: hidden;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.group-item:hover {*/
+///* box-shadow: 0 2px 8px rgba(0,0,0,.1);*/
+///*}*/
+//
+///*.group-item-preview {*/
+///* position: relative;*/
+///* width: 100%;*/
+///* height: 80px;*/
+///* overflow: hidden;*/
+///*}*/
+//
+///*.group-item-preview img {*/
+///* width: 100%;*/
+///* height: 100%;*/
+///* object-fit: cover;*/
+///*}*/
+//
+///*.group-item-overlay {*/
+///* position: absolute;*/
+///* top: 0;*/
+///* right: 0;*/
+///* background: rgba(0,0,0,.7);*/
+///* padding: 4px;*/
+///* opacity: 0;*/
+///* transition: opacity .2s ease;*/
+///*}*/
+//
+///*.group-item:hover .group-item-overlay {*/
+///* opacity: 1;*/
+///*}*/
+//
+///*.remove-from-group {*/
+///* background: #f44336;*/
+///* color: white;*/
+///* border: none;*/
+///* padding: 4px 6px;*/
+///* border-radius: 3px;*/
+///* cursor: pointer;*/
+///* font-size: 10px;*/
+///*}*/
+//
+///*.remove-from-group:hover {*/
+///* background: #d32f2f;*/
+///*}*/
+//
+///*!* Group metadata fields *!*/
+///*.group-metadata {*/
+///* padding-top: 15px;*/
+///* border-top: 1px solid #f0f0f0;*/
+///*}*/
+//
+///*.group-metadata .metadata-field {*/
+///* margin-bottom: 12px;*/
+///*}*/
+//
+///*.group-metadata label {*/
+///* display: block;*/
+///* font-size: 12px;*/
+///* font-weight: 500;*/
+///* color: #333;*/
+///* margin-bottom: 4px;*/
+///*}*/
+//
+///*.group-metadata input,*/
+///*.group-metadata textarea {*/
+///* width: 100%;*/
+///* border: 1px solid #ddd;*/
+///* border-radius: 4px;*/
+///* padding: 6px 8px;*/
+///* font-size: 13px;*/
+///* transition: border-color .2s ease;*/
+///*}*/
+//
+///*.group-metadata input:focus,*/
+///*.group-metadata textarea:focus {*/
+///* outline: none;*/
+///* border-color: #2196F3;*/
+///* box-shadow: 0 0 0 2px rgba(33, 150, 243, .2);*/
+///*}*/
+//
+///*.group-metadata textarea {*/
+///* resize: vertical;*/
+///* min-height: 60px;*/
+///*}*/
+//
+///*!* Drag feedback animations *!*/
+///*@keyframes dragHover {*/
+///* 0% { transform: scale(1); }*/
+///* 50% { transform: scale(1.02); }*/
+///* 100% { transform: scale(1); }*/
+///*}*/
+//
+///*.drag-hover {*/
+///* animation: dragHover .6s ease-in-out infinite;*/
+///*}*/
+//
+///*!* Status indicators for grouped uploads *!*/
+///*.upload-group[data-status="uploading"] {*/
+///* border-color: #FF9800;*/
+///* background: #fff8f0;*/
+///*}*/
+//
+///*.upload-group[data-status="uploaded"] {*/
+///* border-color: #4CAF50;*/
+///* background: #f1f8e9;*/
+///*}*/
+//
+///*.upload-group[data-status="error"] {*/
+///* border-color: #f44336;*/
+///* background: #ffebee;*/
+///*}*/
+//
+///*!* Progress indicators for groups *!*/
+///*.group-progress {*/
+///* height: 3px;*/
+///* background: #f0f0f0;*/
+///* border-radius: 2px;*/
+///* overflow: hidden;*/
+///* margin-top: 10px;*/
+///*}*/
+//
+///*.group-progress-fill {*/
+///* height: 100%;*/
+///* background: #2196F3;*/
+///* transition: width .3s ease;*/
+///* border-radius: 2px;*/
+///*}*/
+//
+///*!* Responsive design *!*/
+///*@media (max-width: 768px) {*/
+///* .upload.field.groupable {*/
+///* grid-template-columns: 1fr;*/
+///* gap: 15px;*/
+///* }*/
+//
+///* .upload.field.groupable .upload-sidebar {*/
+///* border-left: none;*/
+///* border-top: 2px solid #e0e0e0;*/
+///* padding-left: 0;*/
+///* padding-top: 15px;*/
+///* }*/
+//
+///* .group-items {*/
+///* grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));*/
+///* gap: 8px;*/
+///* }*/
+//
+///* .groups-container {*/
+///* max-height: 400px;*/
+///* }*/
+///*}*/
+//
+///*!* Accessibility improvements *!*/
+///*.upload-item.groupable:focus {*/
+///* outline: 2px solid #2196F3;*/
+///* outline-offset: 2px;*/
+///*}*/
+//
+///*.group-drop-zone:focus-within {*/
+///* border-color: #2196F3;*/
+///* background: #f0f8ff;*/
+///*}*/
+//
+///*!* High contrast mode support *!*/
+///*@media (prefers-contrast: high) {*/
+///* .upload-group {*/
+///* border-width: 2px;*/
+///* }*/
+//
+///* .group-drop-zone {*/
+///* border-width: 3px;*/
+///* }*/
+//
+///* .upload-item.groupable.dragging {*/
+///* border: 3px solid #2196F3;*/
+///* }*/
+///*}*/
+//
+///*!* Selection Controls *!*/
+///*.selection-controls {*/
+///* display: flex;*/
+///* align-items: center;*/
+///* gap: 1rem;*/
+///* flex-wrap: wrap;*/
+///*}*/
+//
+///*.selection-actions {*/
+///* display: flex;*/
+///* align-items: center;*/
+///* gap: 1rem;*/
+///* padding: .5rem 1rem;*/
+///* background-color: var(--action-50);*/
+///* border-radius: var(--radius);*/
+///* color: var(--contrast);*/
+///* font-size: .9rem;*/
+///*}*/
+//
+///*.selection-actions button {*/
+///* background: rgba(255, 255, 255, .2);*/
+///* border: 1px solid rgba(255, 255, 255, .3);*/
+///* color: inherit;*/
+///* padding: .25rem .5rem;*/
+///* border-radius: var(--radius);*/
+///* display: flex;*/
+///* align-items: center;*/
+///* gap: .25rem;*/
+///*}*/
+//
+///*.selection-actions button:hover {*/
+///* background: rgba(255, 255, 255, .3);*/
+///*}*/
+//
+///*.selection-count {*/
+///* font-weight: bold;*/
+///*}*/
+//
+///*!* Preview Actions *!*/
+///*.preview-actions {*/
+///* display: flex;*/
+///* justify-content: space-between;*/
+///* align-items: center;*/
+///* padding: 1rem;*/
+///* background-color: var(--base-100);*/
+///* border-radius: var(--radius-outer);*/
+///* margin-bottom: 1rem;*/
+///*}*/
+//
+///*!* Upload Item Enhancements *!*/
+///*.upload-item {*/
+///* position: relative;*/
+///* background: var(--base);*/
+///* border-radius: var(--radius);*/
+///* overflow: hidden;*/
+///* cursor: pointer;*/
+///* transition: transform .2s ease, box-shadow .2s ease;*/
+///*}*/
+//
+///*.upload-item:hover {*/
+///* transform: translateY(-2px);*/
+///* box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);*/
+///*}*/
+//
+///*.upload-item[draggable="true"] {*/
+///* cursor: grab;*/
+///*}*/
+//
+///*.upload-item.dragging {*/
+///* cursor: grabbing;*/
+///* opacity: .5;*/
+///* transform: rotate(5deg) scale(.95);*/
+///*}*/
+//
+///*.upload-item.selected {*/
+///* border: 2px solid var(--action-50);*/
+///* background: rgba(255, 0, 128, .05);*/
+///*}*/
+//
+///*.upload-item .preview {*/
+///* position: relative;*/
+///* aspect-ratio: 1;*/
+///*}*/
+//
+///*.upload-item img {*/
+///* width: 100%;*/
+///* height: 100%;*/
+///* object-fit: cover;*/
+///*}*/
+//
+///*.upload-item .overlay {*/
+///* position: absolute;*/
+///* top: 0;*/
+///* left: 0;*/
+///* right: 0;*/
+///* bottom: 0;*/
+///* background: rgba(0, 0, 0, .6);*/
+///* opacity: 0;*/
+///* transition: opacity .2s ease;*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///* justify-content: space-between;*/
+///* padding: .5rem;*/
+///*}*/
+//
+///*.upload-item:hover .overlay,*/
+///*.upload-item.selected .overlay {*/
+///* opacity: 1;*/
+///*}*/
+//
+///*.upload-item .actions input[type="checkbox"] {*/
+///* margin: 0;*/
+///* transform: scale(1.2);*/
+///*}*/
+//
+///*.upload-item .actions label {*/
+///* margin: 0;*/
+///* padding: 0;*/
+///* cursor: pointer;*/
+///*}*/
+//
+///*!* Status Indicator *!*/
+///*.upload-item .status {*/
+///* position: absolute;*/
+///* top: .5rem;*/
+///* right: .5rem;*/
+///* width: 1.5rem;*/
+///* height: 1.5rem;*/
+///* border-radius: 50%;*/
+///* background: var(--base);*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* box-shadow: 0 2px 4px rgba(0, 0, 0, .2);*/
+///*}*/
+//
+///*.upload-item[data-status="processing"] .status {*/
+///* background: var(--warning);*/
+///*}*/
+//
+///*.upload-item[data-status="uploaded"] .status {*/
+///* background: var(--success);*/
+///*}*/
+//
+///*.upload-item[data-status="error"] .status {*/
+///* background: var(--danger);*/
+///*}*/
+//
+///*!* Group Enhancements *!*/
+///*.upload-group {*/
+///* background: var(--base-50);*/
+///* border-radius: var(--radius-outer);*/
+///* padding: 1rem;*/
+///* margin-bottom: 1rem;*/
+///* border: 2px solid transparent;*/
+///* transition: border-color .2s ease, background-color .2s ease;*/
+///*}*/
+//
+///*.upload-group.dragover {*/
+///* border-color: var(--action-50);*/
+///* background: rgba(255, 0, 128, .05);*/
+///*}*/
+//
+///*.group-header {*/
+///* display: flex;*/
+///* justify-content: space-between;*/
+///* align-items: center;*/
+///* margin-bottom: 1rem;*/
+///*}*/
+//
+///*.group-info {*/
+///* flex: 1;*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///* gap: .25rem;*/
+///*}*/
+//
+///*.group-name {*/
+///* font-size: 1.1rem;*/
+///* font-weight: 500;*/
+///* border: none;*/
+///* background: transparent;*/
+///* color: var(--text);*/
+///* padding: .25rem 0;*/
+///* border-bottom: 1px solid transparent;*/
+///* transition: border-color .2s ease;*/
+///*}*/
+//
+///*.group-name:focus {*/
+///* outline: none;*/
+///* border-bottom-color: var(--action-50);*/
+///*}*/
+//
+///*.group-count {*/
+///* font-size: .85rem;*/
+///* color: var(--text-muted);*/
+///*}*/
+//
+///*.group-actions {*/
+///* display: flex;*/
+///* gap: .5rem;*/
+///*}*/
+//
+///*.group-actions button {*/
+///* background: var(--base);*/
+///* border: 1px solid var(--base-200);*/
+///* border-radius: var(--radius);*/
+///* padding: .5rem;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///*}*/
+//
+///*.group-actions button:hover {*/
+///* background: var(--action-50);*/
+///* color: var(--contrast);*/
+///* border-color: var(--action-50);*/
+///*}*/
+//
+///*.group-actions .delete-group:hover {*/
+///* background: var(--danger);*/
+///* border-color: var(--danger);*/
+///*}*/
+//
+///*!* Group Content *!*/
+///*.group-content {*/
+///* min-height: 120px;*/
+///*}*/
+//
+///*.group-drop-zone {*/
+///* border: 2px dashed var(--base-300);*/
+///* border-radius: var(--radius);*/
+///* padding: 2rem;*/
+///* text-align: center;*/
+///* color: var(--text-muted);*/
+///* transition: all .2s ease;*/
+///* cursor: pointer;*/
+///*}*/
+//
+///*.group-drop-zone:hover,*/
+///*.group-drop-zone.dragover {*/
+///* border-color: var(--action-50);*/
+///* background: rgba(255, 0, 128, .05);*/
+///* color: var(--action-50);*/
+///*}*/
+//
+///*.group-items {*/
+///* display: grid;*/
+///* grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));*/
+///* gap: 1rem;*/
+///* padding: 1rem 0;*/
+///*}*/
+//
+///*.group-item {*/
+///* position: relative;*/
+///* aspect-ratio: 1;*/
+///* border-radius: var(--radius);*/
+///* overflow: hidden;*/
+///* background: var(--base);*/
+///* transition: transform .2s ease;*/
+///*}*/
+//
+///*.group-item:hover {*/
+///* transform: scale(1.02);*/
+///*}*/
+//
+///*.group-item img {*/
+///* width: 100%;*/
+///* height: 100%;*/
+///* object-fit: cover;*/
+///*}*/
+//
+///*.group-item .overlay {*/
+///* position: absolute;*/
+///* top: 0;*/
+///* left: 0;*/
+///* right: 0;*/
+///* bottom: 0;*/
+///* background: rgba(0, 0, 0, .6);*/
+///* opacity: 0;*/
+///* transition: opacity .2s ease;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* gap: .5rem;*/
+///*}*/
+//
+///*.group-item:hover .overlay {*/
+///* opacity: 1;*/
+///*}*/
+//
+///*.group-item .overlay button {*/
+///* background: rgba(255, 255, 255, .9);*/
+///* border: none;*/
+///* border-radius: 50%;*/
+///* width: 2rem;*/
+///* height: 2rem;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.group-item .set-featured {*/
+///* color: var(--warning);*/
+///*}*/
+//
+///*.group-item .set-featured:hover {*/
+///* background: var(--warning);*/
+///* color: white;*/
+///*}*/
+//
+///*.group-item .remove-from-group:hover {*/
+///* background: var(--danger);*/
+///* color: white;*/
+///*}*/
+//
+///*!* Empty Group State *!*/
+///*.empty-group {*/
+///* border: 4px dashed var(--base-200);*/
+///* border-radius: var(--radius);*/
+///* padding: 2rem;*/
+///* text-align: center;*/
+///* color: var(--text-muted);*/
+///* aspect-ratio: 1;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.empty-group:hover,*/
+///*.empty-group.dragover {*/
+///* border-color: var(--action-50);*/
+///* background: rgba(255, 0, 128, .05);*/
+///* color: var(--action-50);*/
+///*}*/
+//
+///*!* Sidebar *!*/
+///*.sidebar {*/
+///* background: var(--base-50);*/
+///* border-radius: var(--radius-outer);*/
+///* padding: 1.5rem;*/
+///* min-height: 400px;*/
+///*}*/
+//
+///*.sidebar .header {*/
+///* margin-bottom: 1.5rem;*/
+///*}*/
+//
+///*.sidebar h4 {*/
+///* margin: 0 0 .5rem 0;*/
+///* color: var(--text);*/
+///*}*/
+//
+///*.sidebar .hint {*/
+///* font-size: .85rem;*/
+///* color: var(--text-muted);*/
+///* margin: .25rem 0;*/
+///*}*/
+//
+///*.new-group {*/
+///* width: 100%;*/
+///* background: var(--action-50);*/
+///* color: var(--contrast);*/
+///* border: none;*/
+///* border-radius: var(--radius);*/
+///* padding: .75rem;*/
+///* margin-bottom: 1rem;*/
+///* cursor: pointer;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* gap: .5rem;*/
+///* font-weight: 500;*/
+///* transition: background-color .2s ease;*/
+///*}*/
+//
+///*.new-group:hover {*/
+///* background: rgba(255, 0, 128, .8);*/
+///*}*/
+//
+///*!* Layout *!*/
+///*.group-display {*/
+///* display: grid;*/
+///* grid-template-columns: 2fr 1fr;*/
+///* gap: 2rem;*/
+///* margin-top: 2rem;*/
+///*}*/
+//
+///*.preview-wrap {*/
+///* min-height: 400px;*/
+///*}*/
+//
+///*.preview-grid {*/
+///* display: grid;*/
+///* grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));*/
+///* gap: 1rem;*/
+///* padding: 1rem;*/
+///* background: var(--base-100);*/
+///* border-radius: var(--radius-outer);*/
+///* min-height: 200px;*/
+///*}*/
+//
+///*.preview-grid:empty::after {*/
+///* content: "No images uploaded yet. Drag files here or click to upload.";*/
+///* grid-column: 1 / -1;*/
+///* text-align: center;*/
+///* color: var(--text-muted);*/
+///* padding: 2rem;*/
+///* border: 2px dashed var(--base-300);*/
+///* border-radius: var(--radius);*/
+///*}*/
+//
+///*!* File Upload Container *!*/
+///*.file-upload-container.dragover {*/
+///* border-color: var(--action-50);*/
+///* background: rgba(255, 0, 128, .05);*/
+///*}*/
+//
+///*!* Responsive Design *!*/
+///*@media (max-width: 768px) {*/
+///* .group-display {*/
+///* grid-template-columns: 1fr;*/
+///* gap: 1rem;*/
+///* }*/
+//
+///* .preview-actions {*/
+///* flex-direction: column;*/
+///* align-items: stretch;*/
+///* gap: 1rem;*/
+///* }*/
+//
+///* .selection-controls {*/
+///* justify-content: space-between;*/
+///* }*/
+//
+///* .preview-grid {*/
+///* grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));*/
+///* }*/
+//
+///* .group-items {*/
+///* grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));*/
+///* }*/
+///*}*/
+//
+///*!* Animation for upload progress *!*/
+///*@keyframes uploadProgress {*/
+///* 0% { transform: translateX(-100%); }*/
+///* 100% { transform: translateX(100%); }*/
+///*}*/
+//
+///*.upload-item[data-status="uploading"] .progress .bar::after {*/
+///* content: '';*/
+///* position: absolute;*/
+///* top: 0;*/
+///* left: 0;*/
+///* height: 100%;*/
+///* width: 20%;*/
+///* background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .5), transparent);*/
+///* animation: uploadProgress 1.5s infinite;*/
+///*}*/
+//
+///*!* Accessibility *!*/
+///*.upload-item:focus-within,*/
+///*.group-item:focus-within,*/
+///*.upload-group:focus-within {*/
+///* outline: 2px solid var(--action-50);*/
+///* outline-offset: 2px;*/
+///*}*/
+//
+///*!* High contrast mode support *!*/
+///*@media (prefers-contrast: high) {*/
+///* .upload-item.selected {*/
+///* border-width: 3px;*/
+///* }*/
+//
+///* .group-drop-zone.dragover {*/
+///* border-width: 3px;*/
+///* }*/
+///*}*/
+///*.restore-notification {*/
+///* background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);*/
+///* border: 1px solid #ffc107;*/
+///* border-left: 4px solid #ff6b35;*/
+///* border-radius: var(--radius-outer);*/
+///* padding: 1.5rem;*/
+///* margin-bottom: 1.5rem;*/
+///* box-shadow: 0 4px 12px rgba(255, 107, 53, .15);*/
+///* animation: slideInFromTop .4s ease-out;*/
+///* transition: opacity .3s ease, transform .3s ease;*/
+///*}*/
+//
+///*@keyframes slideInFromTop {*/
+///* from {*/
+///* transform: translateY(-20px);*/
+///* opacity: 0;*/
+///* }*/
+///* to {*/
+///* transform: translateY(0);*/
+///* opacity: 1;*/
+///* }*/
+///*}*/
+//
+///*.restore-content {*/
+///* display: grid;*/
+///* grid-template-columns: auto 1fr auto;*/
+///* gap: 1rem;*/
+///* align-items: start;*/
+///*}*/
+//
+///*.restore-icon {*/
+///* font-size: 1.5rem;*/
+///* color: #ff6b35;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* width: 2.5rem;*/
+///* height: 2.5rem;*/
+///* background: rgba(255, 107, 53, .1);*/
+///* border-radius: 50%;*/
+///* flex-shrink: 0;*/
+///*}*/
+//
+///*.restore-message {*/
+///* min-width: 0; !* Allow text to wrap *!*/
+///*}*/
+//
+///*.restore-message h4 {*/
+///* margin: 0 0 .5rem 0;*/
+///* color: #d63384;*/
+///* font-size: 1.1rem;*/
+///* font-weight: 600;*/
+///*}*/
+//
+///*.restore-message p {*/
+///* margin: 0 0 .5rem 0;*/
+///* color: #6c5419;*/
+///* line-height: 1.4;*/
+///*}*/
+//
+///*.restore-message p:last-child {*/
+///* margin-bottom: 0;*/
+///*}*/
+//
+///*.restore-message .warning {*/
+///* background: rgba(220, 53, 69, .1);*/
+///* border: 1px solid rgba(220, 53, 69, .2);*/
+///* border-radius: var(--radius);*/
+///* padding: .5rem .75rem;*/
+///* margin-top: .75rem;*/
+///* font-size: .9rem;*/
+///*}*/
+//
+///*.restore-message .warning strong {*/
+///* color: #dc3545;*/
+///*}*/
+//
+///*.restore-actions {*/
+///* display: flex;*/
+///* flex-direction: column;*/
+///* gap: .5rem;*/
+///* flex-shrink: 0;*/
+///*}*/
+//
+///*.start-over-btn {*/
+///* background: #dc3545;*/
+///* color: white;*/
+///* border: none;*/
+///* border-radius: var(--radius);*/
+///* padding: .5rem 1rem;*/
+///* font-size: .9rem;*/
+///* font-weight: 500;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///* white-space: nowrap;*/
+///*}*/
+//
+///*.start-over-btn:hover {*/
+///* background: #c82333;*/
+///* transform: translateY(-1px);*/
+///* box-shadow: 0 2px 4px rgba(220, 53, 69, .3);*/
+///*}*/
+//
+///*.start-over-btn:active {*/
+///* transform: translateY(0);*/
+///*}*/
+//
+///*.dismiss-notification {*/
+///* background: transparent;*/
+///* border: 1px solid #6c5419;*/
+///* color: #6c5419;*/
+///* border-radius: var(--radius);*/
+///* padding: .5rem;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* width: 2rem;*/
+///* height: 2rem;*/
+///*}*/
+//
+///*.dismiss-notification:hover {*/
+///* background: #6c5419;*/
+///* color: white;*/
+///*}*/
+//
+///*!* Start Over Confirmation Dialog *!*/
+///*.start-over-confirmation {*/
+///* border: none;*/
+///* border-radius: var(--radius-outer);*/
+///* padding: 0;*/
+///* box-shadow: 0 10px 30px rgba(0, 0, 0, .3);*/
+///* max-width: 500px;*/
+///* width: 90vw;*/
+///*}*/
+//
+///*.start-over-confirmation::backdrop {*/
+///* background: rgba(0, 0, 0, .6);*/
+///* backdrop-filter: blur(4px);*/
+///*}*/
+//
+///*.confirmation-content {*/
+///* padding: 2rem;*/
+///* background: var(--base);*/
+///* border-radius: var(--radius-outer);*/
+///*}*/
+//
+///*.confirmation-content h3 {*/
+///* margin: 0 0 1rem 0;*/
+///* color: #dc3545;*/
+///* font-size: 1.25rem;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* gap: .5rem;*/
+///*}*/
+//
+///*.confirmation-content h3::before {*/
+///* content: "⚠️";*/
+///* font-size: 1.5rem;*/
+///*}*/
+//
+///*.confirmation-content p {*/
+///* margin: 0 0 1rem 0;*/
+///* color: var(--text);*/
+///* line-height: 1.5;*/
+///*}*/
+//
+///*.confirmation-content ul {*/
+///* margin: 0 0 1rem 1rem;*/
+///* color: var(--text);*/
+///*}*/
+//
+///*.confirmation-content li {*/
+///* margin-bottom: .25rem;*/
+///*}*/
+//
+///*.confirmation-actions {*/
+///* display: flex;*/
+///* gap: 1rem;*/
+///* justify-content: flex-end;*/
+///* margin-top: 1.5rem;*/
+///* padding-top: 1rem;*/
+///* border-top: 1px solid var(--base-200);*/
+///*}*/
+//
+///*.confirm-start-over {*/
+///* background: #dc3545;*/
+///* color: white;*/
+///* border: none;*/
+///* border-radius: var(--radius);*/
+///* padding: .75rem 1.5rem;*/
+///* font-weight: 500;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.confirm-start-over:hover {*/
+///* background: #c82333;*/
+///*}*/
+//
+///*.cancel-start-over {*/
+///* background: var(--base-100);*/
+///* color: var(--text);*/
+///* border: 1px solid var(--base-300);*/
+///* border-radius: var(--radius);*/
+///* padding: .75rem 1.5rem;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.cancel-start-over:hover {*/
+///* background: var(--base-200);*/
+///*}*/
+//
+///*!* Responsive Design *!*/
+///*@media (max-width: 768px) {*/
+///* .restore-content {*/
+///* grid-template-columns: 1fr;*/
+///* gap: 1rem;*/
+///* text-align: center;*/
+///* }*/
+//
+///* .restore-icon {*/
+///* margin: 0 auto;*/
+///* }*/
+//
+///* .restore-actions {*/
+///* flex-direction: row;*/
+///* justify-content: center;*/
+///* align-items: center;*/
+///* }*/
+//
+///* .start-over-btn {*/
+///* order: 2;*/
+///* }*/
+//
+///* .dismiss-notification {*/
+///* order: 1;*/
+///* }*/
+//
+///* .confirmation-actions {*/
+///* flex-direction: column-reverse;*/
+///* }*/
+//
+///* .confirmation-actions button {*/
+///* width: 100%;*/
+///* }*/
+///*}*/
+//
+///*!* Reduced motion preference *!*/
+///*@media (prefers-reduced-motion: reduce) {*/
+///* .restore-notification {*/
+///* animation: none;*/
+///* }*/
+//
+///* .start-over-btn:hover {*/
+///* transform: none;*/
+///* }*/
+///*}*/
+//
+///*!* High contrast mode *!*/
+///*@media (prefers-contrast: high) {*/
+///* .restore-notification {*/
+///* border-width: 2px;*/
+///* border-left-width: 6px;*/
+///* }*/
+//
+///* .restore-message .warning {*/
+///* border-width: 2px;*/
+///* }*/
+///*}*/
+//
+///*!* Dark mode adjustments *!*/
+///*@media (prefers-color-scheme: dark) {*/
+///* .restore-notification {*/
+///* background: linear-gradient(135deg, #3d3d00 0%, #4a4a00 100%);*/
+///* border-color: #ffc107;*/
+///* color: #fff3cd;*/
+///* }*/
+//
+///* .restore-message p {*/
+///* color: #fff3cd;*/
+///* }*/
+//
+///* .restore-message .warning {*/
+///* background: rgba(220, 53, 69, .2);*/
+///* border-color: rgba(220, 53, 69, .4);*/
+///* color: #ffb3ba;*/
+///* }*/
+//
+///* .dismiss-notification {*/
+///* border-color: #fff3cd;*/
+///* color: #fff3cd;*/
+///* }*/
+//
+///* .dismiss-notification:hover {*/
+///* background: #fff3cd;*/
+///* color: #3d3d00;*/
+///* }*/
+///*}*/
+//
+///*.dragover {*/
+///* background-color: rgba(var(--primary-rgb), .1);*/
+///* border: 2px dashed var(--primary-color);*/
+///* border-radius: 8px;*/
+///*}*/
+//
+///*.upload-item.dragging {*/
+///* opacity: .5;*/
+///* transform: scale(.95);*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.group-drop-zone {*/
+///* min-height: 60px;*/
+///* border: 2px dashed #ccc;*/
+///* border-radius: 8px;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* color: #666;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.group-drop-zone.dragover {*/
+///* border-color: var(--primary-color);*/
+///* background-color: rgba(var(--primary-rgb), .05);*/
+///* color: var(--primary-color);*/
+///*}*/
+//
+///*.empty-group {*/
+///* padding: 20px;*/
+///* text-align: center;*/
+///* border: 2px dashed #ddd;*/
+///* border-radius: 8px;*/
+///* margin: 10px 0;*/
+///* cursor: pointer;*/
+///* transition: all .2s ease;*/
+///*}*/
+//
+///*.empty-group:hover {*/
+///* border-color: var(--primary-color);*/
+///* background-color: rgba(var(--primary-rgb), .02);*/
+///*}*/
+//
+///*.group-item.featured {*/
+///* border: 2px solid gold;*/
+///* position: relative;*/
+///*}*/
+//
+///*.group-item.featured::after {*/
+///* content: "⭐";*/
+///* position: absolute;*/
+///* top: 5px;*/
+///* right: 5px;*/
+///* background: gold;*/
+///* border-radius: 50%;*/
+///* width: 20px;*/
+///* height: 20px;*/
+///* display: flex;*/
+///* align-items: center;*/
+///* justify-content: center;*/
+///* font-size: 12px;*/
+///*}*/
+//
+///*.upload-item[draggable="true"] {*/
+///* cursor: grab;*/
+///*}*/
+//
+///*.upload-item[draggable="true"]:active {*/
+///* cursor: grabbing;*/
+///*}*/
+//
+///*.selection-actions {*/
+///* display: flex;*/
+///* align-items: center;*/
+///* gap: 10px;*/
+///* padding: 10px;*/
+///* background: rgba(var(--primary-rgb), .05);*/
+///* border-radius: 6px;*/
+///* border: 1px solid rgba(var(--primary-rgb), .2);*/
+///*}*/
+//
+///*.group-display .sidebar {*/
+///* min-width: 300px;*/
+///* max-width: 350px;*/
+///* background: #f9f9f9;*/
+///* border-left: 1px solid #eee;*/
+///* padding: 20px;*/
+///*}*/
+//
+///*.group-display .preview-wrap {*/
+///* flex: 1;*/
+///* padding: 20px;*/
+///*}*/
+//
+///*.group-display {*/
+///* display: flex;*/
+///* gap: 20px;*/
+///* margin-top: 20px;*/
+///* border: 1px solid #ddd;*/
+///* border-radius: 8px;*/
+///* overflow: hidden;*/
+///*}*/
+//
+///* Touch Drag & Drop Styles */
+//
+///* Original item being dragged */
+//.upload-item.dragging {
+// opacity: .5;
+// transform: scale(.95);
+// transition: opacity .2s ease, transform .2s ease;
+//}
+//
+///* Touch drag preview element */
+//.drag-preview {
+// background: var(--bg-elevated);
+// border: 2px solid var(--accent-primary);
+// cursor: grabbing;
+// user-select: none;
+// -webkit-user-select: none;
+// -webkit-user-drag: none;
+//}
+//
+//.drag-preview * {
+// pointer-events: none;
+//}
+//
+///* Drop target states for touch */
+//.dragover {
+// background-color: var(--accent-primary-alpha);
+// border: 2px dashed var(--accent-primary);
+// transform: scale(1.02);
+// transition: all .2s ease;
+//}
+//
+//.dragover::after {
+// content: "Drop here";
+// position: absolute;
+// top: 50%;
+// left: 50%;
+// transform: translate(-50%, -50%);
+// background: var(--accent-primary);
+// color: white;
+// padding: 8px 16px;
+// border-radius: 4px;
+// font-size: 14px;
+// font-weight: 500;
+// z-index: 1;
+// pointer-events: none;
+//}
+//
+///* Enhanced touch targets for mobile */
+//@media (hover: none) and (pointer: coarse) {
+// .upload-item {
+// min-height: 44px; /* Apple's recommended minimum touch target */
+// touch-action: manipulation;
+// }
+//
+// .upload-item [data-upload-id] {
+// cursor: grab;
+// -webkit-user-select: none;
+// user-select: none;
+// }
+//
+// .upload-item.dragging {
+// cursor: grabbing;
+// }
+//
+// /* Larger touch targets for action buttons */
+// .upload-item .actions button {
+// min-width: 44px;
+// min-height: 44px;
+// padding: 12px;
+// }
+//
+// /* Group containers more touch-friendly */
+// .item-grid.groups .empty-group {
+// padding: 20px;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// font-size: 16px;
+// }
+//
+// .group-content {
+// padding: 0;
+// width: 100%;
+// }
+//
+// /* Better visual feedback for group areas */
+// .item-grid.group,
+// .item-grid.groups {
+// border: 2px solid transparent;
+// border-radius: 8px;
+// transition: border-color .2s ease, background-color .2s ease;
+// }
+//
+// .item-grid.group.dragover,
+// .item-grid.groups.dragover {
+// border-color: var(--accent-primary);
+// background-color: var(--accent-primary-alpha);
+// }
+//}
+//
+///* Haptic feedback animation */
+//@keyframes feedback {
+// 0% { transform: scale(1); }
+// 50% { transform: scale(1.05); }
+// 100% { transform: scale(1); }
+//}
+//
+//.feedback {
+// animation: feedback .2s ease;
+//}
+//
+///* Prevent text selection during touch drag */
+//.dragging,
+//.dragging * {
+// -webkit-user-select: none;
+// -moz-user-select: none;
+// -ms-user-select: none;
+// user-select: none;
+//}
+//
+///* Long press visual feedback */
+//.upload-item:active {
+// transform: scale(.98);
+// transition: transform .1s ease;
+//}
+//
+///* Better spacing for touch interfaces */
+//@media (hover: none) {
+//
+// .item-grid.groups {
+// gap: 16px;
+// }
+//
+// .group-item {
+// margin: 8px;
+// }
+//}
+//
+///* Accessibility improvements for touch */
+//.upload-item:focus-visible {
+// outline: 3px solid var(--accent-primary);
+// outline-offset: 2px;
+//}
+//
+///* High contrast mode support */
+//@media (prefers-contrast: high) {
+// .dragover {
+// border-width: 3px;
+// border-style: solid;
+// }
+//
+// .drag-preview {
+// border-width: 3px;
+// }
+//}
+///* Touch Drag & Drop Styles */
+//
+///* Original item being dragged */
+//.upload-item.dragging {
+// opacity: .5;
+// transform: scale(.95);
+// transition: opacity .2s ease, transform .2s ease;
+//}
+//
+///* Touch drag preview element */
+//.drag-preview {
+// background: var(--bg-elevated);
+// border: 2px solid var(--accent-primary);
+// cursor: grabbing;
+// user-select: none;
+// -webkit-user-select: none;
+// -webkit-user-drag: none;
+//}
+//
+//.drag-preview * {
+// pointer-events: none;
+//}
+//
+///* Multi-item drag preview */
+//.drag-preview.multi-item {
+// /* Container styles handled in JS for dynamic positioning */
+//}
+//.drag-preview img {
+// max-width: 200px;
+// width: 100%;
+// height: 100%;
+// aspect-ratio: 1;
+//}
+//
+//.drag-preview.multi-item .selection-count-badge {
+// background: var(--accent-primary);
+// color: white;
+// border: 2px solid white;
+// font-weight: bold;
+// text-shadow: 0 1px 2px rgba(0,0,0,.5);
+//}
+//
+///* Enhanced visual feedback for multiple selection */
+//.upload-item.selected.dragging {
+// opacity: .3;
+// transform: scale(.9);
+// border: 2px solid var(--accent-primary);
+//}
+//
+///* Stacked preview animation */
+//@keyframes stack-preview {
+// 0% {
+// transform: translateX(0) translateY(0) rotate(0deg) scale(1);
+// opacity: 1;
+// }
+// 100% {
+// transform: translateX(var(--stack-x, 0)) translateY(var(--stack-y, 0)) rotate(var(--stack-rotation, 0deg)) scale(1.05);
+// opacity: var(--stack-opacity, .8);
+// }
+//}
+//
+//.drag-preview.multi-item > * {
+// animation: stack-preview .2s ease-out forwards;
+//}
+//
+///* Drop target states for touch */
+//.dragover {
+// background-color: var(--accent-primary-alpha);
+// border: 2px dashed var(--accent-primary);
+// transform: scale(1.02);
+// transition: all .2s ease;
+//}
+//
+//.dragover::after {
+// content: "Drop here";
+// position: absolute;
+// top: 50%;
+// left: 50%;
+// transform: translate(-50%, -50%);
+// background: var(--accent-primary);
+// color: white;
+// padding: 8px 16px;
+// border-radius: 4px;
+// font-size: 14px;
+// font-weight: 500;
+// z-index: 1;
+// pointer-events: none;
+//}
+//
+///* Enhanced drop feedback for multiple items */
+//.dragover.multi-drop::after {
+// content: "Drop " attr(data-item-count) " items here";
+// padding: 10px 20px;
+// font-size: 16px;
+// border-radius: 6px;
+// box-shadow: 0 4px 12px rgba(0,0,0,.3);
+//}
+//
+///* Pulsing animation for multi-item drop zones */
+//.dragover.multi-drop {
+// animation: multi-drop-pulse 1s infinite alternate;
+//}
+//
+//@keyframes multi-drop-pulse {
+// 0% {
+// background-color: var(--accent-primary-alpha);
+// transform: scale(1.02);
+// }
+// 100% {
+// background-color: var(--accent-secondary-alpha, var(--accent-primary-alpha));
+// transform: scale(1.04);
+// }
+//}
+//
+///* Enhanced touch targets for mobile */
+//@media (hover: none) and (pointer: coarse) {
+// .upload-item {
+// min-height: 44px; /* Apple's recommended minimum touch target */
+// touch-action: manipulation;
+// }
+//
+// .upload-item [data-upload-id] {
+// cursor: grab;
+// -webkit-user-select: none;
+// user-select: none;
+// }
+//
+// .upload-item.dragging {
+// cursor: grabbing;
+// }
+//
+// /* Larger touch targets for action buttons */
+// .upload-item .actions button {
+// min-width: 44px;
+// min-height: 44px;
+// padding: 12px;
+// }
+//
+// /* Group containers more touch-friendly */
+// .item-grid.groups .empty-group {
+// padding: 1rem;
+// margin: 1rem;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// font-size: 16px;
+// }
+//
+// /* Better visual feedback for group areas */
+// .item-grid.group,
+// .item-grid.groups {
+// border: 2px solid transparent;
+// border-radius: 8px;
+// transition: border-color .2s ease, background-color .2s ease;
+// }
+//
+// .item-grid.group.dragover,
+// .item-grid.groups.dragover {
+// border-color: var(--accent-primary);
+// background-color: var(--accent-primary-alpha);
+// }
+//}
+//
+///* Haptic feedback animation */
+//@keyframes feedback {
+// 0% { transform: scale(1); }
+// 50% { transform: scale(1.05); }
+// 100% { transform: scale(1); }
+//}
+//
+//.feedback {
+// animation: feedback .2s ease;
+//}
+//
+///* Prevent text selection during touch drag */
+//.dragging,
+//.dragging * {
+// -webkit-user-select: none;
+// -moz-user-select: none;
+// -ms-user-select: none;
+// user-select: none;
+//}
+//
+///* Long press visual feedback */
+//.upload-item:active {
+// transform: scale(.98);
+// transition: transform .1s ease;
+//}
+//
+///* Better spacing for touch interfaces */
+//@media (hover: none) {
+//
+// .item-grid.groups {
+// gap: 16px;
+// }
+//
+// .group-item {
+// margin: 8px;
+// }
+//}
+//
+///* Accessibility improvements for touch */
+//.upload-item:focus-visible {
+// outline: 3px solid var(--accent-primary);
+// outline-offset: 2px;
+//}
+//
+///* Enhanced selection states for touch */
+//.upload-item.selected {
+// border: 2px solid var(--accent-primary);
+// background-color: var(--accent-primary-alpha);
+//}
+//
+//.upload-item.selected.dragging {
+// border-color: var(--accent-secondary);
+// background-color: var(--accent-secondary-alpha);
+//}
+//
+///* Touch feedback animations */
+//.feedback-start {
+// animation: feedback-start .3s ease;
+//}
+//
+//.feedback-success {
+// animation: feedback-success .5s ease;
+//}
+//
+//.feedback-error {
+// animation: feedback-error .5s ease;
+//}
+//
+//@keyframes feedback-start {
+// 0% { transform: scale(1); }
+// 50% { transform: scale(1.05); background-color: var(--accent-primary-alpha); }
+// 100% { transform: scale(1); }
+//}
+//
+//@keyframes feedback-success {
+// 0% { transform: scale(1); }
+// 25% { transform: scale(1.1); background-color: var(--success-alpha); }
+// 50% { transform: scale(1.05); }
+// 100% { transform: scale(1); }
+//}
+//
+//@keyframes feedback-error {
+// 0% { transform: translateX(0); }
+// 25% { transform: translateX(-5px); background-color: var(--error-alpha); }
+// 50% { transform: translateX(5px); }
+// 75% { transform: translateX(-3px); }
+// 100% { transform: translateX(0); }
+//}
+//
+///* Multi-selection visual enhancements */
+//.upload-item.selected::after {
+// content: "✓";
+// position: absolute;
+// top: 8px;
+// right: 8px;
+// background: var(--accent-primary);
+// color: white;
+// border-radius: 50%;
+// width: 20px;
+// height: 20px;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// font-size: 12px;
+// font-weight: bold;
+// z-index: 10;
+//}
+//
+///* Selection count in drag preview */
+//.selection-count-badge {
+// animation: badge-appear .3s ease;
+//}
+//
+//@keyframes badge-appear {
+// 0% {
+// transform: scale(0);
+// opacity: 0;
+// }
+// 50% {
+// transform: scale(1.2);
+// opacity: 1;
+// }
+// 100% {
+// transform: scale(1);
+// opacity: 1;
+// }
+//}
+//
+///* High contrast mode support */
+//@media (prefers-contrast: high) {
+// .dragover {
+// border-width: 3px;
+// border-style: solid;
+// }
+//
+// .drag-preview {
+// border-width: 3px;
+// }
+//
+// .upload-item.selected {
+// border-width: 3px;
+// }
+//}
+//
+//.drag-preview.multi-item {
+// position: relative;
+// width: 120px !important;
+// height: 120px !important;
+// /* Ensure consistent sizing for multi-item previews */
+//}
+//
+//.drag-preview.multi-item img {
+// max-width: 100%;
+// max-height: 100%;
+// width: 100%;
+// height: 100%;
+// object-fit: cover;
+// aspect-ratio: 1;
+// border-radius: 6px;
+//}
+//
+///* Fix stacked item positioning */
+//.drag-preview.multi-item > .upload-item {
+// position: absolute;
+// width: 100%;
+// height: 100%;
+// border-radius: 6px;
+// overflow: hidden;
+//}
+//
+//.drag-preview.multi-item > .upload-item:nth-child(1) {
+// top: 0;
+// left: 0;
+// z-index: 3;
+// opacity: 1;
+// transform: rotate(-2deg);
+//}
+//
+//.drag-preview.multi-item > .upload-item:nth-child(2) {
+// top: 4px;
+// left: 4px;
+// z-index: 2;
+// opacity: .85;
+// transform: rotate(1deg);
+//}
+//
+//.drag-preview.multi-item > .upload-item:nth-child(3) {
+// top: 8px;
+// left: 8px;
+// z-index: 1;
+// opacity: .7;
+// transform: rotate(-1deg);
+//}
+//
+///* Selection count badge positioning */
+//.drag-preview.multi-item .selection-count-badge {
+// position: absolute;
+// top: -8px;
+// right: -8px;
+// background: var(--accent-primary);
+// color: white;
+// border: 2px solid white;
+// border-radius: 50%;
+// width: 28px;
+// height: 28px;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// font-size: 12px;
+// font-weight: bold;
+// font-family: system-ui, -apple-system, sans-serif;
+// box-shadow: 0 2px 8px rgba(0,0,0,.3);
+// z-index: 20;
+// line-height: 1;
+//}
+//
+///* Ensure drag preview is always visible */
+//.drag-preview {
+// pointer-events: none;
+// z-index: 10000 !important;
+//}
+//
+///* Remove any transforms that might interfere with positioning */
+//.drag-preview.multi-item .upload-item .progress,
+//.drag-preview.multi-item .upload-item .actions,
+//.drag-preview.multi-item .upload-item .status,
+//.drag-preview.multi-item .upload-item details {
+// display: none !important;
+//}
+//
+///* Clean up the visual during multi-drag */
+//.upload-item.selected.dragging {
+// opacity: .3;
+// transform: scale(.95);
+// border: 2px dashed var(--accent-primary);
+// background-color: var(--accent-primary-alpha);
+//}
+//
+///* Improved drop zone feedback for multiple items */
+//.dragover.multi-drop {
+// animation: multi-drop-pulse .8s infinite ease-in-out;
+// border-style: solid;
+//}
+//
+//@keyframes multi-drop-pulse {
+// 0%, 100% {
+// background-color: var(--accent-primary-alpha);
+// transform: scale(1.02);
+// border-color: var(--accent-primary);
+// }
+// 50% {
+// background-color: var(--accent-secondary-alpha, var(--accent-primary-alpha));
+// transform: scale(1.04);
+// border-color: var(--accent-secondary, var(--accent-primary));
+// }
+//}
+//
+///* Enhanced feedback for successful drops */
+//@keyframes drop-success {
+// 0% {
+// background-color: var(--success-alpha);
+// transform: scale(1.1);
+// }
+// 100% {
+// background-color: transparent;
+// transform: scale(1);
+// }
+//}
+//
+//.drop-success {
+// animation: drop-success .6s ease-out;
+//}
+//
+//.drag-preview {
+// border: 2px solid red !important;
+// background: rgba(255, 0, 0, .1) !important;
+//}
+//
+//.drag-preview.multi-item {
+// border: 2px solid blue !important;
+// background: rgba(0, 0, 255, .1) !important;
+//}
+//
+//.dragging {
+// opacity: .5 !important;
+// border: 2px dashed orange !important;
+//}
+//
+//.dragover {
+// background: rgba(0, 255, 0, .2) !important;
+// border: 2px dashed green !important;
+//}
+//
+//.dragover.multi-drop::before {
+// content: "Drop " attr(data-item-count) " items here";
+// position: absolute;
+// top: 50%;
+// left: 50%;
+// transform: translate(-50%, -50%);
+// background: rgba(0, 0, 0, .8);
+// color: white;
+// padding: 8px 16px;
+// border-radius: 4px;
+// font-size: 14px;
+// font-weight: bold;
+// z-index: 1000;
+// pointer-events: none;
+//}
+//
+//.drag-preview {
+// /* Ensure drag preview is always visible */
+// position: fixed !important;
+// z-index: 9999 !important;
+// pointer-events: none !important;
+//
+// /* Improve visual appearance */
+// box-shadow: 0 8px 25px rgba(0,0,0,.3);
+// border-radius: 8px;
+// overflow: hidden;
+//
+// /* Ensure it's not affected by parent transforms or overflow */
+// transform-style: preserve-3d;
+//}
+//
+//.drag-preview.multi-item {
+// /* Ensure proper stacking context for multi-item previews */
+// isolation: isolate;
+//}
+//
+//.drag-preview .upload-item {
+// /* Ensure child items are positioned correctly */
+// border-radius: 4px;
+// overflow: hidden;
+// box-shadow: 0 2px 8px rgba(0,0,0,.15);
+//}
+//
+//.drag-preview .upload-item img {
+// /* Prevent image layout issues during drag */
+// max-width: 100%;
+// height: auto;
+// display: block;
+//}
+//
+///* Debug styles - remove in production */
+//.drag-preview {
+// border: 2px solid red !important;
+//}
+//
+//.drag-preview.multi-item {
+// border: 2px solid blue !important;
+//}
+//
+///* Hide complex UI elements in drag preview */
+//.drag-preview details,
+//.drag-preview .actions,
+//.drag-preview .progress {
+// display: none !important;
+//}
+//
+///* Simplify the preview to just show the image */
+//.drag-preview .preview {
+// width: 100% !important;
+// height: 100% !important;
+//}
+//
+//.drag-preview .preview img {
+// width: 100% !important;
+// height: 100% !important;
+// object-fit: cover !important;
+//}
+//
+//
+//.upload-group summary {
+// padding: 0!important;
+//}
+//
+//
+///* Mobile-First Image Grouping Layout - Using Existing Templates */
+//
+///* Base mobile layout - stack everything vertically */
+//@media (max-width: 768px) {
+//
+// .item-grid.preview details,
+// .item-grid.group details {
+// display: none;
+// }
+// .group-display {
+// display: flex;
+// flex-direction: column;
+// gap: 0;
+// height: calc(100vh - var(--btn) - var(--btn));
+// position: fixed;
+// top: var(--btn);
+// bottom: var(--btn);
+// left: 0;
+// right: 0;
+// z-index:999;
+// }
+//
+// .preview-wrap,
+// .sidebar {
+// flex: 1;
+// display: flex;
+// flex-direction: column;
+// min-height: 0;
+// overflow: hidden;
+// overflow-y: auto;
+// }
+// /* Preview section - top half of screen */
+// .preview-wrap {
+// background: var(--base);
+// border-bottom: 2px solid var(--action-0);
+// }
+//
+// /* Preview actions - sticky at top */
+// .preview-actions {
+// position: sticky;
+// top: 0;
+// z-index: 10;
+// background: var(--base);
+// border-bottom: 1px solid var(--base-200);
+// flex-shrink: 0;
+// }
+//
+// /* Preview grid - scrollable area */
+// .item-grid.preview {
+// flex: 1;
+// grid-template-columns: repeat(3, 1fr);
+// gap: .75rem;
+// padding: .5rem;
+// margin-top: 0;
+// }
+// .item-grid + .hint {
+// margin-top: 0;
+// }
+//
+// /* Groups section - bottom half with sticky header */
+// .sidebar {
+// background: var(--base-200);
+// border-top: 3px solid var(--action-50);
+// }
+//
+// /* Groups header - sticky */
+// .sidebar .header {
+// border-bottom: 1px solid var(--base-200);
+// padding: 1rem;
+// flex-shrink: 0;
+// }
+//
+// /* Create group button */
+// .create-group-from-selection {
+// margin: 0 1rem 1rem 1rem;
+// flex-shrink: 0;
+// min-height: 44px; /* Touch target */
+// padding: .75rem 1rem;
+// font-size: .9rem;
+// }
+//
+// /* Groups container - scrollable */
+// .item-grid.groups {
+// flex: 1;
+// margin: 0;
+// grid-template-columns: repeat(2, 1fr); /* Single column on mobile */
+// gap: 1rem;
+// }
+//
+// /* Enhanced empty group for mobile */
+// .empty-group {
+// aspect-ratio: unset;
+// grid-column: 1/-1;
+// transition: all .2s ease;
+// }
+//
+// .item-grid.groups .item-grid {
+// grid-template-columns: repeat(3, 1fr);
+// }
+// .item-grid.groups .item-grid details {
+// display: none;
+// }
+//
+// .empty-group:hover,
+// .empty-group.dragover {
+// border-color: var(--action-0);
+// background: var(--action-rgb-subtle-hover);
+// transform: scale(1.02);
+// }
+//
+// /* Upload button - fixed at bottom */
+// .submit-uploads {
+// position: fixed !important;
+// bottom: calc(var(--btn) + .5rem);
+// right: .5rem;
+// z-index: 20;
+// height: 3rem;
+// font-size: 1.1rem;
+// font-weight: 600;
+// box-shadow: rgba(var(--base-rgb),var(--op-45)) var(--shdw);
+// border-radius: var(--radius-outer);
+// }
+//
+// .submit-uploads:hover {
+// transform: translateY(-2px);
+// box-shadow: 0 8px 25px rgba(0,0,0,.2);
+// }
+//
+// /* Enhanced upload items for mobile */
+// .upload-item {
+// border-radius: var(--radius);
+// overflow: hidden;
+// background: var(--base);
+// border: 1px solid var(--base-200);
+// transition: transform .2s ease;
+// }
+//
+// .upload-item:hover {
+// transform: scale(1.02);
+// }
+//
+// /* Mobile-friendly actions overlay */
+// .upload-item .actions {
+// padding: .5rem;
+// }
+//
+// .upload-item .actions button {
+// min-width: 44px;
+// min-height: 44px;
+// padding: .75rem;
+// border-radius: var(--radius);
+// }
+//
+// /* Better checkbox targets */
+// .upload-item .upload-select + label {
+// min-width: 44px;
+// min-height: 44px;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// border-radius: var(--radius);
+// }
+//
+// /* Enhanced group styling for mobile */
+// .upload-group {
+// background: var(--base-100);
+// border-radius: var(--radius);
+// border: 1px solid var(--base-200);
+// padding: 1rem;
+// margin-bottom: 1rem;
+// }
+//
+// .upload-group .group-header {
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+// margin-bottom: 1rem;
+// padding-bottom: .5rem;
+// border-bottom: 1px solid var(--base-200);
+// flex-wrap: wrap;
+// gap: .5rem;
+// }
+//
+// .upload-group .group-actions {
+// display: flex;
+// gap: .5rem;
+// flex-wrap: wrap;
+// }
+//
+// .upload-group .group-actions button {
+// flex: 1;
+// min-width: 100px;
+// min-height: 44px;
+// padding: .5rem .75rem;
+// font-size: .9rem;
+// border-radius: var(--radius);
+// }
+//
+// .upload-group .item-grid.group {
+// grid-template-columns: repeat(2, 1fr);
+// gap: .5rem;
+// margin-bottom: 1rem;
+// }
+//
+// .upload-group .group-item {
+// aspect-ratio: 1;
+// border-radius: 4px;
+// overflow: hidden;
+// border: 1px solid var(--base-200);
+// }
+//
+// /* Hide file upload container on mobile when in grouping mode */
+// .group-display:not([hidden]) ~ .file-upload-container {
+// display: none;
+// }
+//
+// /* Enhanced selection controls for mobile */
+// .selection-controls {
+// display: flex;
+// flex-direction: column;
+// gap: 0;
+// }
+//
+// .selection-controls .selected {
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+// gap: .5rem;
+// }
+//
+// .selection-actions {
+// display: flex;
+// padding: 0;
+// }
+//
+// .selection-actions button {
+// flex: 1;
+// padding: .25rem;
+// font-size: .9rem;
+// border-radius: var(--radius);
+// }
+//
+// /* Enhanced dragging states for mobile */
+// .upload-item.dragging {
+// opacity: .7;
+// transform: scale(.95) rotate(3deg);
+// z-index: 1000;
+// box-shadow: 0 8px 25px rgba(0,0,0,.3);
+// }
+//
+// .dragover {
+// background: var(--action-rgb-subtle) !important;
+// border-color: var(--action-0) !important;
+// transform: scale(1.05);
+// animation: mobile-drop-pulse .8s infinite ease-in-out;
+// }
+//
+// @keyframes mobile-drop-pulse {
+// 0%, 100% {
+// background-color: var(--action-rgb-subtle);
+// transform: scale(1.02);
+// }
+// 50% {
+// background-color: var(--action-rgb-subtle-hover);
+// transform: scale(1.04);
+// }
+// }
+//
+// /* Enhanced selection states */
+// .upload-item.selected {
+// border: 2px solid var(--action-0);
+// background-color: var(--action-rgb-subtle);
+// }
+//
+// .upload-item.selected::after {
+// content: '✓';
+// position: absolute;
+// top: .5rem;
+// right: .5rem;
+// background: var(--action-0);
+// color: white;
+// border-radius: 50%;
+// width: 24px;
+// height: 24px;
+// display: flex;
+// align-items: center;
+// justify-content: center;
+// font-size: 12px;
+// font-weight: bold;
+// z-index: 10;
+// animation: selection-pop .3s ease;
+// }
+//
+// @keyframes selection-pop {
+// 0% {
+// transform: scale(0);
+// opacity: 0;
+// }
+// 70% {
+// transform: scale(1.2);
+// opacity: 1;
+// }
+// 100% {
+// transform: scale(1);
+// opacity: 1;
+// }
+// }
+//
+// /* Enhanced details/summary for mobile */
+// .upload-item details summary {
+// padding: .75rem;
+// background: var(--base-100);
+// border-radius: var(--radius);
+// cursor: pointer;
+// display: flex;
+// align-items: center;
+// gap: .5rem;
+// font-size: .9rem;
+// font-weight: 500;
+// min-height: 44px; /* Touch target */
+// }
+//
+// .upload-item details[open] summary {
+// border-radius: var(--radius) var(--radius) 0 0;
+// border-bottom: 1px solid var(--base-200);
+// }
+//
+// /* Enhanced forms for mobile */
+// .upload-meta input,
+// .upload-meta textarea {
+// padding: .75rem;
+// font-size: 16px; /* Prevents zoom on iOS */
+// border-radius: var(--radius);
+// border: 2px solid var(--base-200);
+// transition: border-color .2s ease;
+// }
+//
+// .upload-meta input:focus,
+// .upload-meta textarea:focus {
+// border-color: var(--action-0);
+// outline: none;
+// box-shadow: 0 0 0 3px var(--action-rgb-subtle);
+// }
+//}
+//
+///* Tablet adjustments */
+//@media (min-width: 769px) and (max-width: 1024px) {
+// .group-display {
+// grid-template-columns: 1fr;
+// gap: 1.5rem;
+// }
+//
+// .item-grid.preview {
+// grid-template-columns: repeat(4, 1fr);
+// }
+//
+// .item-grid.groups {
+// grid-template-columns: repeat(2, 1fr);
+// }
+//}
+//
+//
+//body:has(.group-display:not([hidden])) {
+// overflow:hidden;
+//}
+//
+//.today_hours .group-fields {
+// width: 100%;
+// display: flex;
+// justify-content: center;
+// gap: 3rem;
+//}
+//.today_hours .field {
+// margin: 0;
+//}
+//
+//
+//.jvb-integration-connection {
+// background: var(--bg-white, #EFEFEF);
+// border: 2px solid #ddd;
+// border-radius: 8px;
+// padding: 1.5rem;
+// margin-bottom: 1.5rem;
+// position: relative;
+// transition: all 0.3s ease;
+//}
+//
+//.jvb-integration-connection.connected {
+// border-color: #22c55e;
+// box-shadow: 0 2px 8px rgba(34, 197, 94, 0.1);
+//}
+//
+//.jvb-integration-connection.disconnected {
+// border-color: #ef4444;
+// box-shadow: 0 2px 8px rgba(239, 68, 68, 0.1);
+//}
+//
+///* Integration Grid */
+//.jvb-integrations-grid {
+// display: grid;
+// grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+// gap: 1.5rem;
+// margin-top: 1rem;
+//}
+//
+///* Connection Header */
+//.jvb-connection-header {
+// display: flex;
+// justify-content: space-between;
+// align-items: flex-start;
+// margin-bottom: 1.5rem;
+// padding-bottom: 1rem;
+// border-bottom: 2px solid #ddd;
+//}
+//
+//.jvb-service-info h3.jvb-service-name {
+// margin: 0 0 0.25rem 0;
+// font-size: 1.4rem;
+// font-weight: bold;
+// text-transform: uppercase;
+// letter-spacing: 1px;
+// color: var(--bg-black, #1B1B1B);
+// font-family: 'Courier New', monospace;
+//}
+//
+//.jvb-service-description {
+// margin: 0;
+// color: #666;
+// font-size: 0.9rem;
+// line-height: 1.4;
+//}
+//
+//.jvb-connection-status {
+// text-align: right;
+// min-width: 150px;
+//}
+//
+//.jvb-status-indicator {
+// font-size: 1.2rem;
+// margin-right: 0.5rem;
+//}
+//
+//.jvb-connection-status.connected .jvb-status-indicator {
+// color: #22c55e;
+//}
+//
+//.jvb-connection-status.disconnected .jvb-status-indicator {
+// color: #ef4444;
+//}
+//
+//.jvb-status-text {
+// font-weight: bold;
+// font-size: 0.9rem;
+// text-transform: uppercase;
+// letter-spacing: 0.5px;
+// font-family: 'Courier New', monospace;
+//}
+//
+//.jvb-last-tested {
+// display: block;
+// color: #888;
+// font-size: 0.75rem;
+// margin-top: 0.25rem;
+//}
+//
+///* Settings Section */
+//.jvb-settings-section,
+//.jvb-integration-settings {
+// margin-bottom: 1.5rem;
+//}
+//
+//.jvb-integration-settings h4 {
+// margin: 0 0 1rem 0;
+// font-size: 1.1rem;
+// font-weight: bold;
+// color: var(--bg-black, #1B1B1B);
+// font-family: 'Courier New', monospace;
+// text-transform: uppercase;
+// letter-spacing: 0.5px;
+//}
+//
+///* Form Fields */
+//.jvb-form-field {
+// margin-bottom: 1rem;
+//}
+//
+//.jvb-form-field label {
+// display: block;
+// font-weight: bold;
+// margin-bottom: 0.5rem;
+// color: var(--bg-black, #1B1B1B);
+// font-family: 'Courier New', monospace;
+// text-transform: uppercase;
+// font-size: 0.85rem;
+// letter-spacing: 0.5px;
+//}
+//
+//.jvb-form-field input,
+//.jvb-form-field select {
+// width: 100%;
+// padding: 0.75rem;
+// border: 2px solid #ddd;
+// border-radius: 4px;
+// font-size: 0.9rem;
+// background: white;
+// transition: border-color 0.3s ease;
+//}
+//
+//.jvb-form-field input:focus,
+//.jvb-form-field select:focus {
+// outline: none;
+// border-color: var(--action-color, #FF0080);
+// box-shadow: 0 0 0 3px rgba(255, 0, 128, 0.1);
+//}
+//
+//.jvb-form-field .description {
+// font-size: 0.8rem;
+// color: #666;
+// margin-top: 0.5rem;
+// line-height: 1.4;
+//}
+//
+///* Checkbox Labels */
+//.jvb-checkbox-label {
+// display: flex !important;
+// align-items: center;
+// gap: 0.5rem;
+// text-transform: none !important;
+// font-weight: normal !important;
+// cursor: pointer;
+//}
+//
+//.jvb-checkbox-label input[type="checkbox"] {
+// width: auto !important;
+// margin: 0;
+//}
+//
+///* Advanced Settings */
+//.jvb-advanced-settings {
+// margin-top: 1rem;
+// padding: 1rem;
+// background: #f9f9f9;
+// border-radius: 4px;
+// border: 1px solid #e5e5e5;
+//}
+//
+//.jvb-advanced-settings summary {
+// font-weight: bold;
+// cursor: pointer;
+// margin-bottom: 1rem;
+// color: var(--action-color, #FF0080);
+// font-family: 'Courier New', monospace;
+// text-transform: uppercase;
+// font-size: 0.85rem;
+// letter-spacing: 0.5px;
+//}
+//
+///* Settings Help */
+//.jvb-settings-help {
+// background: #f0f8ff;
+// border: 1px solid #b3d9ff;
+// border-radius: 4px;
+// padding: 1rem;
+// margin-top: 1rem;
+//}
+//
+//.jvb-settings-help ol {
+// margin: 0.5rem 0 0 1.5rem;
+// padding: 0;
+//}
+//
+//.jvb-settings-help a {
+// color: var(--action-color, #FF0080);
+// text-decoration: none;
+// font-weight: bold;
+//}
+//
+//.jvb-settings-help a:hover {
+// text-decoration: underline;
+//}
+//
+///* Connection Actions */
+//.jvb-connection-actions {
+// display: flex;
+// gap: 0.75rem;
+// flex-wrap: wrap;
+// border-top: 2px solid #ddd;
+// padding-top: 1rem;
+// align-items: center;
+//}
+//
+//.jvb-connection-actions .button {
+// font-family: 'Courier New', monospace;
+// font-weight: bold;
+// text-transform: uppercase;
+// letter-spacing: 0.5px;
+// border-radius: 4px;
+// padding: 0.6rem 1.2rem;
+// font-size: 0.8rem;
+// cursor: pointer;
+// transition: all 0.2s ease;
+//}
+//
+//.jvb-connect-button.button-primary {
+// background: var(--action-color, #FF0080);
+// border-color: var(--action-color, #FF0080);
+// color: white;
+// box-shadow: 0 2px 4px rgba(255, 0, 128, 0.2);
+//}
+//
+//.jvb-connect-button.button-primary:hover {
+// background: #e6007a;
+// border-color: #e6007a;
+// transform: translateY(-1px);
+// box-shadow: 0 4px 8px rgba(255, 0, 128, 0.3);
+//}
+//
+//.jvb-test-button {
+// background: white;
+// border-color: #22c55e;
+// color: #22c55e;
+//}
+//
+//.jvb-test-button:hover {
+// background: #22c55e;
+// color: white;
+//}
+//
+//.jvb-sync-button {
+// background: white;
+// border-color: var(--action-color, #FF0080);
+// color: var(--action-color, #FF0080);
+//}
+//
+//.jvb-sync-button:hover {
+// background: var(--action-color, #FF0080);
+// color: white;
+//}
+//
+//.jvb-disconnect-button {
+// color: #ef4444 !important;
+// border: none !important;
+// background: none !important;
+// text-decoration: underline;
+// margin-left: auto;
+//}
+//
+//.jvb-disconnect-button:hover {
+// color: #dc2626 !important;
+// background: none !important;
+//}
+//
+///* Loading States */
+//.jvb-integration-form.loading .button {
+// opacity: 0.7;
+// pointer-events: none;
+//}
+//
+//.jvb-integration-form.loading .jvb-connect-button::after {
+// content: ' (Saving...)';
+//}
+//
+//.jvb-integration-form.loading .jvb-test-button::after {
+// content: ' (Testing...)';
+//}
+//
+///* Responsive Design */
+//@media (max-width: 768px) {
+// .jvb-integrations-grid {
+// grid-template-columns: 1fr;
+// }
+//
+// .jvb-connection-header {
+// flex-direction: column;
+// gap: 1rem;
+// }
+//
+// .jvb-connection-status {
+// text-align: left;
+// min-width: auto;
+// }
+//
+// .jvb-connection-actions {
+// flex-direction: column;
+// }
+//
+// .jvb-connection-actions .button {
+// width: 100%;
+// text-align: center;
+// }
+//
+// .jvb-disconnect-button {
+// margin-left: 0 !important;
+// }
+//}
+//
+//form .tabs {
+// position: sticky;
+// top: calc(var(--btn) + 2rem);
+// left: 0;
+// right: 0;
+// z-index: 50;
+// background: var(--base);
+//}
+//
+//.form-actions {
+// display: flex;
+//}
+//
+//.spinner {
+// width: 12px;
+// height: 12px;
+// border: 2px solid transparent;
+// border-top: 2px solid var(--action-50);
+// border-radius: 50%;
+// animation: spin 1s linear infinite;
+//}
+//
+//@keyframes spin {
+// 0% { transform: rotate(0deg); }
+// 100% { transform: rotate(360deg); }
+//}
diff --git a/src/forms/view.js b/src/forms/view.js
new file mode 100644
index 0000000..788e929
--- /dev/null
+++ b/src/forms/view.js
@@ -0,0 +1,112 @@
+/**
+ * view.js
+ * Frontend JavaScript for the Form Block
+ * Handles form validation and submission
+ */
+/**
+ * view.js
+ * Frontend JavaScript for the Form Block
+ */
+class FormBlock {
+ constructor() {
+ this.controller = window.jvbForm;
+
+ document.querySelectorAll('.jvb-form-block form').forEach(form => {
+ this.controller.registerForm(form, {
+ cache: true,
+ autoUpload: false,
+ imageMeta: false,
+ });
+ });
+
+ this.controller.subscribe((event, data) => {
+ if (event === 'form-submit') {
+ this.handleFormSubmission(data).then(()=>{});
+ }
+ });
+ }
+
+ async handleFormSubmission(eventData) {
+ const { config, data } = eventData;
+ const form = config.element;
+
+ const submitData = new FormData();
+
+ // Add regular form fields
+ for (const [key, value] of Object.entries(data)) {
+ if (Array.isArray(value)) {
+ value.forEach(v => submitData.append(`${key}[]`, v));
+ } else if (typeof value === 'object' && value !== null) {
+ submitData.append(key, JSON.stringify(value));
+ } else {
+ submitData.append(key, value);
+ }
+ }
+ config.element.querySelectorAll('[name="form_id"],[name="form_type"],[name="timestamp"],[name="cf-turnstile-response"]').forEach(input => {
+ submitData.append(input.name, input.value);
+ });
+
+ // Add uploaded files
+ if (window.jvbUploads) {
+ try {
+ const files = await window.jvbUploads.getFilesForForm(form);
+
+ files.forEach(({ file, fieldName }) => {
+ submitData.append(`${fieldName}[]`, file);
+ });
+ } catch (error) {
+ console.error('Error getting files:', error);
+ }
+ }
+
+ this.controller.showFormStatus(config.id, 'uploading');
+
+ try {
+ const response = await fetch(`${jvbSettings.api}forms`, {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: submitData
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ this.controller.showFormStatus(config.id, 'error');
+ this.controller.handleFormError(form, result);
+ return;
+ }
+
+ this.controller.showFormStatus(config.id, 'submitted');
+ // this.controller.handleFormSuccess(form, result);
+ this.controller.showSummary({ changes: data, config: config });
+ window.jvbA11y.announce('Form successfully submitted!');
+
+ // Clean up uploaded files
+ if (window.jvbUploads) {
+ const uploadFields = form.querySelectorAll('[data-upload-field]');
+ for (const field of uploadFields) {
+ const fieldId = window.jvbUploads.determineFieldId(field);
+ await window.jvbUploads.clearFieldFromStores(fieldId);
+ }
+ }
+
+ } catch (error) {
+ console.error('Form submission error:', error);
+ this.controller.showFormStatus(config.id, 'error');
+ this.controller.handleFormError(form, {
+ message: 'Network error. Please check your connection and try again.',
+ code: 'network_error'
+ });
+ } finally {
+ await this.controller.store.delete(config.id);
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ new FormBlock();
+ }
+ });
+});
diff --git a/src/glossary/block.json b/src/glossary/block.json
new file mode 100644
index 0000000..2705831
--- /dev/null
+++ b/src/glossary/block.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/glossary",
+ "version": "0.1.0",
+ "title": "Glossary of Terms",
+ "category": "jvb",
+ "icon": "excerpt-view",
+ "description": "Outputs the terms",
+ "example": {},
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "textdomain": "jvb",
+ "selectors": {
+ "root": ".glossary"
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/glossary/edit.js b/src/glossary/edit.js
new file mode 100644
index 0000000..4a115af
--- /dev/null
+++ b/src/glossary/edit.js
@@ -0,0 +1,38 @@
+/**
+ * Retrieves the translation of text.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * Those files can contain any CSS code that gets applied to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './editor.scss';
+
+/**
+ * The edit function describes the structure of your block in the context of the
+ * editor. This represents what the editor will render when the block is used.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
+ *
+ * @return {Element} Element to render.
+ */
+export default function Edit() {
+ return (
+ <p { ...useBlockProps() }>
+ { __( 'Will output the glossary', 'jvb' ) }
+ </p>
+ );
+}
diff --git a/src/glossary/editor.scss b/src/glossary/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/glossary/editor.scss
diff --git a/src/glossary/index.js b/src/glossary/index.js
new file mode 100644
index 0000000..d82621b
--- /dev/null
+++ b/src/glossary/index.js
@@ -0,0 +1,33 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+} );
diff --git a/src/glossary/index.php b/src/glossary/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/glossary/index.php
diff --git a/src/glossary/render.php b/src/glossary/render.php
new file mode 100644
index 0000000..52aded4
--- /dev/null
+++ b/src/glossary/render.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
+ */
+?>
+<p <?php echo get_block_wrapper_attributes(); ?>>
+ <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?>
+</p>
diff --git a/src/glossary/style.scss b/src/glossary/style.scss
new file mode 100644
index 0000000..a367991
--- /dev/null
+++ b/src/glossary/style.scss
@@ -0,0 +1,109 @@
+:root {
+ --navWidth: 40vw;
+ @media (min-width: 768px) {
+ --navWidth: 22vw;
+ }
+}
+
+nav.glossary-index {
+ position: fixed;
+ top: 50%;
+ transform: translateY(-50%);
+ width: var(--navWidth);
+ right: -8px;
+ height: 60vh;
+ z-index: var(--z-3);
+
+ > ul {
+ --dir: column;
+ --align: flex-start;
+ --justify: flex-start;
+ --gap: 1px;
+ touch-action: pan-y;
+ max-height: 100%;
+ height: 100%;
+ width: 100%;
+ overflow: hidden auto;
+ scroll-behavior: smooth;
+ }
+ li, a {
+ flex: 1;
+ width: 100%;
+ height: max-content;
+ min-height: max(var(--chipchip), max-content);
+ }
+ a {
+ --justify: center;
+ padding: .25rem .5rem;
+ hyphens: auto;
+ background-color: rgba(var(--base-rgb),var(--op-45));
+ word-wrap: anywhere;
+ white-space: wrap;
+ }
+ a:hover,
+ a:focus,
+ a.active {
+ background-color: rgba(var(--action-rgb), var(--op-6));
+ color: var(--action-contrast);
+ }
+}
+.glossary dd {
+ margin-left: .5rem;
+ width: calc(100% + .75rem);
+}
+.glossary dd,
+.glossary dt {
+ position: relative;
+ left: 0;
+ transition: margin var(--trans-base),
+ left var(--trans-base),
+ width var(--trans-base);
+}
+.glossary dt:target,
+.glossary dt.active {
+ outline: none;
+ left: -1.5rem;
+ padding: 0;
+ color: var(--action-0);
+}
+ .glossary dt:target + dd,
+ .glossary dt.active + dd {
+ left: -1.5rem;
+ }
+
+main header,
+dl.glossary {
+ grid-column: full;
+ padding: 0 var(--navWidth) 0 2rem;
+ @media (min-width:768px) {
+ margin-left: auto;
+ max-width: var(--content);
+ margin-right: var(--navWidth);
+ padding-right: var(--btn);
+ }
+}
+
+@media (max-width: 768px) {
+ .glossary {
+ h2 {
+ font-size: var(--txt-medium);
+ }
+ p {
+ font-size: var(--txt-x-small);
+ }
+ }
+ .glossary-index {
+ li,a {
+ height: fit-content;
+ }
+ a {
+ font-size: var(--txt-x-small);
+ padding: .25rem;
+ min-height: 2em;
+ }
+ }
+
+ body:has(.glossary) h1 {
+ font-size: var(--txt-xx-large);
+ }
+}
diff --git a/src/glossary/view.js b/src/glossary/view.js
new file mode 100644
index 0000000..f68d59a
--- /dev/null
+++ b/src/glossary/view.js
@@ -0,0 +1,184 @@
+/**
+ * Glossary Navigation Active State Manager
+ * Handles highlighting active terms as they scroll into view
+ * and syncing navigation with scroll position
+ */
+class GlossaryNavigator {
+ constructor(glossarySelector = 'dl.glossary', navSelector = 'nav.glossary-index') {
+ this.glossary = document.querySelector(glossarySelector);
+ this.nav = document.querySelector(navSelector);
+
+ if (!this.glossary || !this.nav) return;
+
+ this.terms = this.glossary.querySelectorAll('dt[id]');
+ this.navList = this.nav.querySelector('ul');
+ this.activeClass = 'active';
+ this.currentActive = null;
+
+ this.init();
+ this.setupResizeHandler();
+ }
+
+ init() {
+ // Set up Intersection Observer with screen-size appropriate margins
+ const observerOptions = {
+ root: null, // viewport
+ rootMargin: this.getRootMargin(),
+ threshold: 0
+ };
+
+ this.observer = new IntersectionObserver(
+ (entries) => this.handleIntersection(entries),
+ observerOptions
+ );
+
+ // Observe all terms
+ this.terms.forEach(term => this.observer.observe(term));
+
+ // Also handle manual scroll for edge cases
+ this.handleScroll = this.debounce(() => this.checkActiveTerm(), 100);
+ window.addEventListener('scroll', this.handleScroll, { passive: true });
+ }
+
+ getRootMargin() {
+ // On larger screens: centered (50% from top and bottom)
+ return '-50% 0px -50% 0px';
+ }
+
+ setupResizeHandler() {
+ let resizeTimer;
+ window.addEventListener('resize', () => {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(() => {
+ // Reinitialize observer with new margins on resize
+ this.reinitialize();
+ }, 250);
+ });
+ }
+
+ reinitialize() {
+ // Disconnect old observer
+ if (this.observer) {
+ this.observer.disconnect();
+ }
+
+ // Create new observer with updated margins
+ this.init();
+ }
+
+ handleIntersection(entries) {
+ // Find the entry that's intersecting
+ const intersecting = entries.find(entry => entry.isIntersecting);
+
+ if (intersecting) {
+ this.setActive(intersecting.target);
+ }
+ }
+
+ checkActiveTerm() {
+ // Fallback method to find which term is in the trigger zone
+ const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize);
+ const margin = remInPixels * 4;
+
+ let closestTerm = null;
+ let closestDistance = Infinity;
+
+ this.terms.forEach(term => {
+ const rect = term.getBoundingClientRect();
+
+ const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight;
+
+ if (isInZone) {
+ // Find closest to the trigger point
+ const triggerPoint = window.innerHeight / 2;
+
+ const distance = Math.abs(rect.top - triggerPoint);
+
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closestTerm = term;
+ }
+ }
+ });
+
+ if (closestTerm) {
+ this.setActive(closestTerm);
+ }
+ }
+
+ setActive(term) {
+ if (this.currentActive === term) return;
+
+ // Remove active class from previous term
+ if (this.currentActive) {
+ this.currentActive.classList.remove(this.activeClass);
+ }
+
+ // Add active class to current term
+ term.classList.add(this.activeClass);
+ this.currentActive = term;
+
+ // Update navigation
+ this.updateNavigation(term.id);
+ }
+
+ updateNavigation(termId) {
+ // Remove active from all nav links
+ const navLinks = this.nav.querySelectorAll('a');
+ navLinks.forEach(link => link.classList.remove(this.activeClass));
+
+ // Find and activate corresponding nav link
+ const activeLink = this.nav.querySelector(`a[href="#${termId}"]`);
+
+ if (activeLink) {
+ activeLink.classList.add(this.activeClass);
+
+ // Scroll the nav list to center the active link
+ this.centerNavItem(activeLink);
+ }
+ }
+
+ centerNavItem(link) {
+ const listRect = this.navList.getBoundingClientRect();
+ const linkRect = link.getBoundingClientRect();
+
+ // Calculate position to center the link in the nav container
+ const scrollTop = this.navList.scrollTop;
+ const linkOffset = linkRect.top - listRect.top;
+ const centerOffset = (listRect.height / 2) - (linkRect.height / 2);
+
+ this.navList.scrollTo({
+ top: scrollTop + linkOffset - centerOffset,
+ behavior: 'smooth'
+ });
+ }
+
+ debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ }
+
+ destroy() {
+ if (this.observer) {
+ this.observer.disconnect();
+ }
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+}
+
+
+// Initialize when DOM is ready
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ new GlossaryNavigator();
+ });
+} else {
+ new GlossaryNavigator();
+}
diff --git a/src/gmbreviews/block.json b/src/gmbreviews/block.json
new file mode 100644
index 0000000..78634fd
--- /dev/null
+++ b/src/gmbreviews/block.json
@@ -0,0 +1,68 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/gmbreviews",
+ "title": "GMB Reviews",
+ "category": "jvb",
+ "description": "Display top-rated Google My Business reviews with statistics and action buttons",
+ "keywords": ["reviews", "google", "testimonials", "gmb", "ratings"],
+ "textdomain": "jvb",
+ "attributes": {
+ "inheritUser": {
+ "type": "boolean",
+ "default": false
+ },
+ "count": {
+ "type": "number",
+ "default": 5
+ },
+ "showRating": {
+ "type": "boolean",
+ "default": true
+ },
+ "showDate": {
+ "type": "boolean",
+ "default": true
+ },
+ "showReviewLink": {
+ "type": "boolean",
+ "default": true
+ },
+ "showViewAllLink": {
+ "type": "boolean",
+ "default": true
+ },
+ "showStats": {
+ "type": "boolean",
+ "default": true
+ },
+ "minStars": {
+ "type": "number",
+ "default": 4,
+ "minimum": 1,
+ "maximum": 5
+ }
+ },
+ "supports": {
+ "html": false,
+ "align": true,
+ "color": {
+ "text": true,
+ "background": true,
+ "link": true
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true
+ }
+ },
+ "render": "file:./render.php",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/gmbreviews/edit.js b/src/gmbreviews/edit.js
new file mode 100644
index 0000000..7f7e09f
--- /dev/null
+++ b/src/gmbreviews/edit.js
@@ -0,0 +1,69 @@
+// src/gmbreviews/edit.js
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import ServerSideRender from '@wordpress/server-side-render';
+
+export default function Edit({ attributes, setAttributes }) {
+ const blockProps = useBlockProps();
+ const { count, inheritUser, showStats, minStars, showViewAllLink, showRating, showDate, showReviewLink } = attributes;
+
+ return (
+ <>
+ <InspectorControls>
+ <PanelBody title={__('Review Settings', 'jvb')}>
+ <ToggleControl
+ label={__('Inherit User', 'jvb')}
+ checked={inheritUser}
+ onChange={(value) => setAttributes({ inheritUser: value })}
+ />
+ <RangeControl
+ label={__('Number of Reviews', 'jvb')}
+ value={count}
+ onChange={(value) => setAttributes({ count: value })}
+ min={1}
+ max={20}
+ />
+ <ToggleControl
+ label={__('Show Rating', 'jvb')}
+ checked={showRating}
+ onChange={(value) => setAttributes({ showRating: value })}
+ />
+ <ToggleControl
+ label={__('Show Date', 'jvb')}
+ checked={showDate}
+ onChange={(value) => setAttributes({ showDate: value })}
+ />
+ <ToggleControl
+ label={__('Show Review Link', 'jvb')}
+ checked={showReviewLink}
+ onChange={(value) => setAttributes({ showReviewLink: value })}
+ />
+ <ToggleControl
+ label={__('Show Stats', 'jvb')}
+ checked={showStats}
+ onChange={(value) => setAttributes({ showStats: value })}
+ />
+ <ToggleControl
+ label={__('Show All Reviews Link', 'jvb')}
+ checked={showViewAllLink}
+ onChange={(value) => setAttributes({ showViewAllLink: value })}
+ />
+ <RangeControl
+ label={__('Minimum Rating', 'jvb')}
+ value={minStars}
+ onChange={(value) => setAttributes({ minStars: value })}
+ min={1}
+ max={5}
+ />
+ </PanelBody>
+ </InspectorControls>
+ <div {...blockProps}>
+ <ServerSideRender
+ block="jvb/gmbreviews"
+ attributes={attributes}
+ />
+ </div>
+ </>
+ );
+}
diff --git a/src/gmbreviews/editor.scss b/src/gmbreviews/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/gmbreviews/editor.scss
diff --git a/src/gmbreviews/index.js b/src/gmbreviews/index.js
new file mode 100644
index 0000000..2f2dd15
--- /dev/null
+++ b/src/gmbreviews/index.js
@@ -0,0 +1,11 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from './edit';
+import './style.scss';
+import './editor.scss';
+import metadata from './block.json';
+
+registerBlockType(metadata.name, {
+ edit: Edit,
+ // No save function - dynamic block rendered on server
+ save: () => null,
+});
diff --git a/src/gmbreviews/index.php b/src/gmbreviews/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/gmbreviews/index.php
diff --git a/src/gmbreviews/render.php b/src/gmbreviews/render.php
new file mode 100644
index 0000000..487f394
--- /dev/null
+++ b/src/gmbreviews/render.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * GMB Reviews Block - Render Template
+ *
+ * Displays recent Google My Business reviews with a link to leave a review
+ */
+function jvbRenderGMBReviewsBlock(array $attributes): string
+{
+ $count = $attributes['count'] ?? 5;
+ $showRating = $attributes['showRating'] ?? true;
+ $showDate = $attributes['showDate'] ?? true;
+ $showReviewLink = $attributes['showReviewLink'] ?? true;
+ $showViewAllLink = $attributes['showViewAllLink'] ?? true;
+ $showStats = $attributes['showStats'] ?? true;
+ $minStars = $attributes['minStars'] ?? 4; // Only show 4+ star reviews
+ $inheritUser = $attributes['inheritUser']??null;
+ if ($inheritUser) {
+ global $post;
+ $inheritUser = $post->post_author;
+ }else {
+ $inheritUser = null;
+ }
+ try {
+ $gmb = JVB()->connect('gmb', $inheritUser);
+ if (!$gmb->isSetUp()) {
+ error_log('GMB Not set up for: '.(int)$inheritUser);
+ return '';
+ }
+ $gotReviews = $gmb->getReviews();
+ // Get all data
+ $allReviews = $gotReviews['reviews']??[];
+ $reviewUrl = $gmb->getReviewUrl();
+ $viewAllUrl = $gmb->getReviewsViewUrl();
+
+ $average = $gotReviews['averageRating']??null;
+ $total = $gotReviews['totalReviewCount']??null;
+
+ // Filter reviews by minimum stars
+ $reviews = [];
+ if (!empty($allReviews)) {
+ foreach ($allReviews as $review) {
+ $rating = $review['starRating'] ?? 0;
+ if ($rating >= $minStars) {
+ $reviews[] = $review;
+ if (count($reviews) >= $count) {
+ break; // Got enough reviews
+ }
+ }
+ }
+ }
+
+ if (empty($reviews) && empty($reviewUrl) && empty($stats)) {
+ error_log('No reviews to display...');
+ return '';
+ }
+
+ ob_start();
+ ?>
+ <div class="gmb-reviews">
+ <div class="row center">
+ <?php
+ if ($showStats && !empty($average) && !empty($total)) {
+ ?>
+ <p>
+ <span class="stars" title="<?= $average ?> out of 5 stars">
+ <?php
+ $fullStars = floor($average);
+ $hasHalfStar = ($average - $fullStars) >= 0.5;
+
+ for ($i = 1; $i <= 5; $i++) {
+ if ($i <= $fullStars) {
+ echo jvbIcon('star', ['style' => 'fill']);
+ } elseif ($i == $fullStars + 1 && $hasHalfStar) {
+ echo jvbIcon('star-half', ['style'=> 'fill']);
+ } else {
+ echo jvbIcon('star', ['style' => 'light']);
+ }
+ }
+ ?>
+ </span>
+ <i>Average</i>
+ </p>
+ <?php
+ if ($total > 0) {
+ ?>
+ <p><i>{ <?= number_format($total ) . ' ' . _n('Review', 'Reviews', $total, 'jvb')?> Total }</i></p>
+ <?php
+ }
+ ?>
+ <?php
+ }
+ ?>
+
+
+ </div>
+ <?php
+ if ($showReviewLink && !empty($reviewUrl)) {
+ ?>
+ <a href="<?=esc_url($reviewUrl)?>"
+ class="button"
+ target="_blank"
+ rel="noopener noreferrer">
+ <?= jvbIcon('star', ['style' => 'fill']) ?>
+ Leave Your Review
+ </a>
+ <?php
+ }
+ ?>
+
+ <ul>
+ <?php
+ foreach ($reviews as $review) {
+ $reviewer = $review['reviewer']['displayName'] ?? 'Anonymous';
+ $reviewer = strtok($reviewer, ' ');
+ $profilePhoto = $review['reviewer']['profilePhotoUrl'] ?? '';
+ $rating = $review['starRating'] ?? 0;
+ $rating = match($rating) {
+ 'FIVE' => 5,
+ 'FOUR' => 4,
+ 'THREE' => 3,
+ 'TWO' => 2,
+ 'ONE' => 1,
+ default => $rating
+ };
+ $comment = $review['comment'] ?? '';
+ $date = $review['updateTime'] ?? '';
+ ?>
+ <li>
+ <blockquote class="review">
+ <?php
+ // Review text
+ if (!empty($comment)) { ?>
+ <div class="content review">
+ <?= apply_filters('wpautop', $comment) ?>
+ </div>
+ <?php } ?>
+ <cite class="row left nowrap">
+ <?php if (!empty($profilePhoto)) { ?>
+ <img src="<?=esc_url($profilePhoto)?>"
+ alt="<?=esc_attr($reviewer)?>"
+ 'loading="lazy">
+ <?php } else { ?>
+ <div class="avatar">
+ <?= jvbIcon('user-circle')?>
+ </div>
+ <?php } ?>
+
+ <div class="row left wrap">
+ <?php if ($showRating && $rating > 0) { ?>
+ <div class="stars" title="<?= $rating ?> out of 5 stars">
+ <?php
+ for ($i = 1; $i <= 5; $i++) {
+ echo ($i <= $rating) ? jvbIcon('star', ['style' => 'fill']) : jvbIcon('star', ['style' => 'light']);
+ } ?>
+ </div>
+ <?php } ?>
+ <p><?= esc_html($reviewer)?></p>
+ <?php
+ // Date
+ if ($showDate && !empty($date)) {
+ $formatted_date = human_time_diff(strtotime($date), current_time('timestamp')) . ' ago';
+ ?>
+ <time datetime="<?=esc_attr($date)?>">
+ <?= esc_html($formatted_date) ?>
+ </time>
+ <?php } ?>
+
+ </div>
+ </cite>
+ </blockquote>
+ </li>
+ <?php
+ }
+ ?>
+ </ul>
+ <?php
+ // Footer with "See All Reviews" button
+ if ($showViewAllLink && !empty($viewAllUrl)) {
+ ?>
+ <div class="footer">
+ <a href=" <?= esc_url($viewAllUrl) ?>"
+ class="button"
+ target="_blank"
+ rel="noopener noreferrer">
+
+ <?php
+ if ($showStats ) {
+ echo 'See All ' . number_format($total) . ' Reviews';
+ } else {
+ echo ' See All Reviews';
+ }
+ ?>
+ <?= jvbIcon('arrow-square-out') ?>
+ </a>
+ </div>
+ <?php
+ }
+ ?>
+ </div>
+ <?php
+ return ob_get_clean();
+
+ } catch (\Exception $e) {
+ error_log('[GMB Reviews Block] Error: ' . $e->getMessage());
+ return '';
+ }
+}
diff --git a/src/gmbreviews/style.scss b/src/gmbreviews/style.scss
new file mode 100644
index 0000000..19f3abb
--- /dev/null
+++ b/src/gmbreviews/style.scss
@@ -0,0 +1,122 @@
+.gmb-reviews {
+ max-width: none;
+ > .row.center {
+ max-width:var(--content);
+ margin: 0 auto;
+ --gap: .5rem 6rem;
+
+ p {
+ width: fit-content;
+ }
+ }
+ .button {
+ width: 66.6%;
+ margin: 0 auto 2rem;
+ display: flex;
+ height: max-content;
+ }
+ .stars {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-start;
+ flex-wrap: nowrap;
+ }
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-width: var(--full);
+ li {
+ width: 100%;
+ max-width: none;
+ padding: 4rem 1rem;
+ &:nth-of-type(odd) {
+ background-color: var(--base-50);
+ blockquote {
+ --background: var(--base-50);
+ }
+ }
+ &:nth-of-type(even) {
+ background-color: var(--base-100);
+ blockquote {
+ --background: var(--base-100);
+ }
+ }
+ }
+ }
+ blockquote {
+ margin:0 auto;
+ padding: 0;
+ max-width: var(--content);
+ .content {
+ border-width: 4px 1px;
+ &::after {
+ border-width: 4px 1px;
+ }
+ &::before {
+ border-width: 8px;
+ bottom: -4px;
+ }
+ }
+ cite {
+ position: relative;
+
+ img {
+ width: 4.5rem;
+ position: absolute;
+ left: -8rem;
+ top: 0;
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .wrap {
+ --wrap: wrap;
+
+ p, time {
+ max-width: 49%;
+ }
+
+ .stars {
+ width: 100%;
+ }
+ }
+ }
+ time {
+ white-space: nowrap;
+ }
+ }
+ .stars .icon {
+ background-color: var(--action-0);
+ }
+ article {
+ padding: 1rem;
+ border-radius: var(--radius-outer);
+ background-color: var(--base);
+ header {
+ --align: center;
+ >img {
+ position: relative;
+ left: 0;
+ }
+ }
+ time {
+ font-style: italic;
+ }
+ .review {
+ padding: 1.5rem;
+ }
+
+ h4 {
+ width: max-content;
+ }
+ .icon {
+ color: var(--action-0);
+ }
+ }
+ .footer .button {
+ width: 100%;
+ }
+}
diff --git a/src/gmbreviews/view.js b/src/gmbreviews/view.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/gmbreviews/view.js
diff --git a/src/index.php b/src/index.php
new file mode 100644
index 0000000..4f2541d
--- /dev/null
+++ b/src/index.php
@@ -0,0 +1,3 @@
+<?php
+//Nothing to see here
+
diff --git a/src/menu/block.json b/src/menu/block.json
new file mode 100644
index 0000000..2f198c3
--- /dev/null
+++ b/src/menu/block.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/menu",
+ "version": "0.1.0",
+ "title": "Our Menu",
+ "category": "jvb",
+ "icon": "food",
+ "description": "Outputs our menu, organized by categories",
+ "example": {},
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "textdomain": "jvb",
+ "selectors": {
+ "root": ".menu-block"
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/menu/edit.js b/src/menu/edit.js
new file mode 100644
index 0000000..cbb91c1
--- /dev/null
+++ b/src/menu/edit.js
@@ -0,0 +1,38 @@
+/**
+ * Retrieves the translation of text.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * Those files can contain any CSS code that gets applied to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './editor.scss';
+
+/**
+ * The edit function describes the structure of your block in the context of the
+ * editor. This represents what the editor will render when the block is used.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
+ *
+ * @return {Element} Element to render.
+ */
+export default function Edit() {
+ return (
+ <p { ...useBlockProps() }>
+ { __( 'Will output the menu', 'jvb' ) }
+ </p>
+ );
+}
diff --git a/src/menu/editor.scss b/src/menu/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/menu/editor.scss
diff --git a/src/menu/index.js b/src/menu/index.js
new file mode 100644
index 0000000..d82621b
--- /dev/null
+++ b/src/menu/index.js
@@ -0,0 +1,33 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+} );
diff --git a/src/menu/index.php b/src/menu/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/menu/index.php
diff --git a/src/menu/render.php b/src/menu/render.php
new file mode 100644
index 0000000..52aded4
--- /dev/null
+++ b/src/menu/render.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
+ */
+?>
+<p <?php echo get_block_wrapper_attributes(); ?>>
+ <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?>
+</p>
diff --git a/src/menu/style.scss b/src/menu/style.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/menu/style.scss
diff --git a/src/menu/view.js b/src/menu/view.js
new file mode 100644
index 0000000..153b0df
--- /dev/null
+++ b/src/menu/view.js
@@ -0,0 +1,43 @@
+window.details = document.querySelectorAll('details');
+window.toggles = document.querySelectorAll('.toggle-details');
+
+document.addEventListener('click', (e) => {
+ if (e.target.classList.contains('toggle-details')) {
+ e.target.classList.toggle('open');
+ let on = e.target.classList.contains('open');
+ let section = e.target.dataset.toggle;
+ if (section === 'all') {
+ toggleToggles(on);
+ }
+
+ let span = e.target.querySelector('span');
+ span.textContent = (on) ? 'Close': 'Open';
+ toggleDetails(section, on);
+ }
+});
+
+
+console.log(window.details);
+function toggleDetails(name, toggle) {
+ if (name === 'all') {
+ console.log('Toggling all!');
+ window.details.forEach(detail => {
+ console.log(detail);
+ detail.open = toggle;
+ });
+ } else {
+ for (let detail of window.details) {
+ if (detail.dataset.section === name) {
+ detail.open = toggle;
+ }
+ }
+ }
+}
+
+function toggleToggles(on) {
+ window.toggles.forEach(toggle => {
+ if (toggle.dataset.toggle !== 'all') {
+ toggle.querySelector('span').textContent = (on) ? 'Close' : 'Open';
+ }
+ });
+}
diff --git a/src/summary/block.json b/src/summary/block.json
new file mode 100644
index 0000000..2cca715
--- /dev/null
+++ b/src/summary/block.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/summary",
+ "title": "Archive Summary",
+ "category": "jvb",
+ "icon": "align-center",
+ "description": "Outputs the information for the given archive page, or the bio for a profile. Pairs well with the feed block.",
+ "keywords": [ "summary", "bio", "style", "term" ],
+ "version": "0.9.0",
+ "textdomain": "jvb",
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "selectors": {
+ "root": ".summary-block"
+ },
+ "styles": [
+ { "name": "default", "label": "Default", "isDefault": true }
+ ],
+ "example": {
+ "attributes": {
+ "listType": "tattoo"
+ }
+ },
+ "render": "file:./render.php",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/summary/edit.js b/src/summary/edit.js
new file mode 100644
index 0000000..6741773
--- /dev/null
+++ b/src/summary/edit.js
@@ -0,0 +1,29 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
+import { SelectControl, ToggleControl, PanelBody } from '@wordpress/components';
+
+/**
+ * Styles
+ */
+import './editor.scss';
+
+/**
+ * Edit function for Summary Block
+ */
+export default function Edit({ attributes, setAttributes }) {
+ const blockProps = useBlockProps();
+
+ return (
+ <div {...blockProps}>
+ <div className="jvb-summary-preview">
+ <h3>{__('Summary', 'jvb')}</h3>
+ <p className="jvb-list-preview-note">
+ {__('This will inherit the current query to build the information from our custom meta on the front end.', 'jvb')}
+ </p>
+ </div>
+ </div>
+ );
+}
diff --git a/src/summary/editor.scss b/src/summary/editor.scss
new file mode 100644
index 0000000..bdf5776
--- /dev/null
+++ b/src/summary/editor.scss
@@ -0,0 +1,20 @@
+/**
+ * Directory List Block Editor Styles
+ */
+.jvb-summary-preview {
+ padding: 20px;
+ background-color: #f8f9fa;
+ border: 1px solid #e2e4e7;
+ border-radius: 4px;
+
+ h3 {
+ margin-top: 0;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ff0080;
+ }
+ &-note {
+ font-style: italic;
+ color: #555d66;
+ margin-bottom: 0;
+ }
+}
diff --git a/src/summary/index.js b/src/summary/index.js
new file mode 100644
index 0000000..c477d23
--- /dev/null
+++ b/src/summary/index.js
@@ -0,0 +1,39 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import save from './save';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+
+ /**
+ * @see ./save.js
+ */
+ save,
+} );
diff --git a/src/summary/index.php b/src/summary/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/summary/index.php
diff --git a/src/summary/render.php b/src/summary/render.php
new file mode 100644
index 0000000..c2c35fb
--- /dev/null
+++ b/src/summary/render.php
@@ -0,0 +1,320 @@
+<?php
+
+use JVBase\managers\Cache;
+use JVBase\meta\Meta;
+use JVBase\meta\Render;
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+ exit; // Exit if accessed directly
+}
+/**
+ * Summary Block Render
+ *
+ * @package Edmonton_Ink
+ */
+
+function jvbRenderSummaryBlock(array $attributes):string
+{
+
+ // Buffer output
+ if (is_tax()) {
+ switch (get_queried_object()->taxonomy) {
+ case BASE.'shop':
+ return jvbRenderShopSummary();
+ default:
+ return jvbRenderTermSummary();
+ }
+ } elseif (is_singular()) {
+ return jvbRenderArtistSummary();
+ }
+ return '';
+}
+
+function jvbRenderArtistSummary():string
+{
+ $current = get_queried_object();
+ $cache = Cache::for('artistSummary', WEEK_IN_SECONDS);
+ $key = $current->ID;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+ $meta = Meta::forPost($current->ID);
+ $artist = jvbContentFromUser((int)$current->post_author);
+
+ $registrar = Registrar::getInstance($current->post_type));
+ $sections = [];
+ if ($registrar) {
+ $sections = $registrar->getSections();
+ }
+
+
+
+
+// $handler = JVB()->getContent(str_replace(BASE,'', $current->post_type));
+ ?>
+ <nav id="artist" class="on-this-page index">
+ <label>Jump to:
+ <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
+ <?= jvbIcon('plus-square')?>
+ </button>
+ </label>
+ <ul>
+ <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li>
+ <li><a href="#about">About</a></li>
+ <li><a href="#styles">Styles</a></li>
+ <li><a href="#contact">Contact</a></li>
+ <li><a href="#work">Work</a></li>
+ </ul>
+ </nav>
+ <header id="top">
+ <h1><small><?=(!empty($artist['city'])) ? $artist['city']['name'] :'Edmonton'?>'s Best <?= (!empty($artist['type'])) ?
+ $artist['type']['name']:'Tattoo Artists'?>:
+ </small><?=$artist['display_name']?></h1>
+ <div>
+ <?php if (!empty($artist['shop'])) : ?>
+ <ul class="term-list shop">
+ <li>
+ <a href="<?=$artist['shop']['url']?>" title="Learn more about <?=$artist['shop']['name']?>">
+ <?= strtolower($artist['shop']['name'])?>
+ </a>
+ </li>
+ </ul>
+ <?php endif; ?>
+ <?php if (!empty($artist['city'])): ?>
+ <ul class="term-list city">
+ <li>
+ <a href="<?=$artist['city']['url']?>" title="See who else is rocking out of <?=$artist['city']['name']?>">
+ <?= strtolower($artist['city']['name'])?>
+ </a>
+ </li>
+ </ul>
+ <?php endif; ?>
+ <?php $styles = $meta->get('top_styles');
+ if (!empty($styles)) {
+ ?>
+ <ul class="term-list style">
+ <?php
+ foreach ($styles as $style) {
+ $term = get_term((int)$style, BASE.'style');
+ if ($term && !is_wp_error($term)) {
+ $link = get_term_link((int)$style, BASE.'style');
+ ?>
+ <li>
+ <a href="<?=$link?>" title="Learn more about <?=html_entity_decode($term->name)?>">
+ <?=strtolower(html_entity_decode($term->name))?>
+ </a>
+ </li>
+ <?php
+ }
+ }
+ ?>
+ </ul>
+ <?php
+ }
+ ?>
+ </div>
+ </header>
+ <section>
+ <details class="bio-info">
+ <summary class="row x-btw">
+ <h2>About <?= ($artist['name'] !== '') ? $artist['name'] : strtok($artist['display_name'], ' ')?></h2>
+ </summary>
+ <div class="columns stack-small">
+ <div class="column">
+ <?= Render::renderFrom($meta, 'image_portrait'); ?>
+ </div>
+ <div class="column">
+ <?= Render::renderFrom($meta, 'short_bio'); ?>
+ </div>
+ </div>
+ <div id="styles">
+ <h3>Works In</h3>
+ <?= jvbGetTheTerms('style', $current->ID) ?>
+ </div>
+ <div class="contact">
+ <h3>Contact:</h3>
+ <?php
+ echo jvbRenderContactInfo($current->ID, $meta);
+ echo jvbRenderLinks($current->ID, $meta);
+ ?>
+ </div>
+
+ <div id="about">
+ <?= Render::renderFrom($meta, 'bio')?>
+ </div>
+ </details>
+ </section>
+ <section id="contact" class="">
+ <h2>Contact <?=$artist['name']?></h2>
+ <?php
+ echo jvbRenderContactInfo($current->ID, 'post');
+ echo jvbRenderLinks($current->ID, 'post');
+ ?>
+ </section>
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
+
+function jvbRenderShopSummary()
+{
+ $current = get_queried_object();
+
+ $cache = Cache::for('shop_bio', WEEK_IN_SECONDS)->connect('taxonomy');
+ $key = $current->term_id;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+
+ $meta = Meta::forTerm($current->term_id);
+ $fields = $meta->getAll(['average_rating', 'established', 'bio','location','hours','specialties','awards','reviews']);
+ ?>
+ <nav id="shop" class="on-this-page index">
+ <label>Jump to:
+ <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
+ <?= jvbIcon('plus-square')?>
+ </button>
+ </label>
+ <ul>
+ <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php
+ if ($fields['rating'] !== 'none') {
+ ?>
+ <li><a href="#rating">Rating</a></li>
+ <?php
+ } elseif ($fields['opened'] !== '') {
+ ?>
+ <li><a href="#opened">Opened</a></li>
+ <?php
+ } elseif ($fields['location'] !== '') {
+ ?>
+ <li><a href="#location">Location</a></li>
+ <?php
+ } elseif ($fields['about'] !== '') {
+ ?>
+ <li><a href="#about">About</a></li>
+ <?php
+ } elseif ($fields['hours'] !== '') {
+ ?>
+ <li><a href="#hours">Hours</a></li>
+ <?php
+ } elseif ($fields['specialties'] !== '') {
+ ?>
+ <li><a href="#specialties">Specialties</a></li>
+ <?php
+ } elseif ($fields['awards'] !== '') {
+ ?>
+ <li><a href="#awards">Awards</a></li>
+ <?php
+ } elseif ($fields['reviews'] !== '') {
+ ?>
+ <li><a href="#reviews">Reviews</a></li>
+ <?php
+ }
+ ?>
+ <li><a href="#contact">Contact</a></li>
+ <li><a href="#artists">Artists</a></li>
+ </ul>
+ </nav>
+ <header id="top">
+ <div class="columns stack-small">
+ <div class="column">
+ <?=jvbFormatImage($meta->get('image'))?>
+ </div>
+ <div class="column">
+ <h1>
+ <small><?= (get_term((int)$meta->get('city'), BASE.'city')) ?
+ get_term((int)$meta->get('city'), BASE.'city')->name :
+ 'Edmonton'?>'s Best Tattoo Shops</small>
+ <?=$current->name?>
+ </h1>
+ <?= jvbFormatRating($current->term_id, 'term') ?>
+ <?= Render::renderFrom($meta, 'slogan'); ?>
+ </div>
+ </div>
+ </header>
+ <section>
+ <details class="bio-info">
+ <summary class="row x-btw">
+ <h2>Learn More About <?=$current->name?></h2>
+ </summary>
+ <div class="map">
+ <?= Render::renderFrom($meta, 'location'); ?>
+ </div>
+ <div class="short-bio">
+ <?= Render::renderFrom($meta, 'short_bio'); ?>
+ </div>
+
+ <div class="contact">
+ <h3>Contact:</h3>
+ <?php
+ echo jvbRenderContactInfo($current->term_id, 'term');
+ echo jvbRenderLinks($current->term_id, 'term');
+ ?>
+ </div>
+
+ <div id="about">
+ <?= Render::renderFrom($meta, 'bio')?>
+ </div>
+ </details>
+ </section>
+ <section id="contact" class="">
+ <h2>Contact </h2>
+ <?php
+ echo jvbRenderContactInfo($current->term_id, 'term');
+ echo jvbRenderLinks($current->term_id, 'term');
+ ?>
+ </section>
+ <?= jvbRenderHours($current->term_id, 'term')?>
+
+
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
+
+
+function jvbRenderTermSummary()
+{
+ $current = get_queried_object();
+ $cache = Cache::for('term_summary', WEEK_IN_SECONDS)->connect('taxonomy');
+ $key = $current->ID;
+ $cached = $cache->get($key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ ob_start();
+ $tax = jvbNoBase($current->taxonomy);
+ switch ($tax) {
+ case 'style':
+ $title = 'Tattoo Artists';
+ break;
+ case 'theme':
+ $title = 'Tattoos';
+ break;
+ default:
+ $title = '';
+ }
+
+ $meta = Meta::forTerm($current->ID);
+ $fields = $meta->getAll();
+
+ ?>
+ <header id="top">
+ <h1><?= get_the_archive_title() ?></h1>
+ </header>
+
+ <?php
+ $finished = ob_get_clean();
+ $cache->set($key, $finished);
+ return $finished;
+}
diff --git a/src/summary/save.js b/src/summary/save.js
new file mode 100644
index 0000000..8169594
--- /dev/null
+++ b/src/summary/save.js
@@ -0,0 +1,3 @@
+export default function save() {
+ return null; // Dynamic block rendered by PHP
+}
diff --git a/src/summary/style.scss b/src/summary/style.scss
new file mode 100644
index 0000000..b182fe9
--- /dev/null
+++ b/src/summary/style.scss
@@ -0,0 +1,20 @@
+details > div {
+ margin: 1rem 0;
+}
+
+main > header:not(:has(img)) {
+ margin-top: 3rem!important;
+}
+
+header a::before {
+ display: none!important;
+}
+
+header + details {
+ margin: 1.5rem auto 3rem!important;
+ max-width: var(--wide);
+}
+
+main {
+ padding-top: 0!important;
+}
diff --git a/src/summary/view.js b/src/summary/view.js
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/summary/view.js
@@ -0,0 +1 @@
+
diff --git a/src/timeline/block.json b/src/timeline/block.json
new file mode 100644
index 0000000..9e219a1
--- /dev/null
+++ b/src/timeline/block.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/timeline",
+ "version": "0.1.0",
+ "title": "Timeline",
+ "category": "jvb",
+ "icon": "shortcode",
+ "description": "Outputs a single timeline post in a cool way",
+ "example": {},
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"]
+ },
+ "textdomain": "jvb",
+ "selectors": {
+ "root": ".timeline"
+ },
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css",
+ "viewScript": "file:./view.js"
+}
diff --git a/src/timeline/edit.js b/src/timeline/edit.js
new file mode 100644
index 0000000..14f0ce6
--- /dev/null
+++ b/src/timeline/edit.js
@@ -0,0 +1,38 @@
+/**
+ * Retrieves the translation of text.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * Those files can contain any CSS code that gets applied to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './editor.scss';
+
+/**
+ * The edit function describes the structure of your block in the context of the
+ * editor. This represents what the editor will render when the block is used.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
+ *
+ * @return {Element} Element to render.
+ */
+export default function Edit() {
+ return (
+ <p { ...useBlockProps() }>
+ { __( 'Will output the timeline', 'jvb' ) }
+ </p>
+ );
+}
diff --git a/src/timeline/editor.scss b/src/timeline/editor.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/editor.scss
diff --git a/src/timeline/index.js b/src/timeline/index.js
new file mode 100644
index 0000000..d82621b
--- /dev/null
+++ b/src/timeline/index.js
@@ -0,0 +1,33 @@
+/**
+ * Registers a new block provided a unique name and an object defining its behavior.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
+ * All files containing `style` keyword are bundled together. The code used
+ * gets applied both to the front of your site and to the editor.
+ *
+ * @see https://www.npmjs.com/package/@wordpress/scripts#using-css
+ */
+import './style.scss';
+
+/**
+ * Internal dependencies
+ */
+import Edit from './edit';
+import metadata from './block.json';
+
+/**
+ * Every block starts by registering a new block type definition.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
+ */
+registerBlockType( metadata.name, {
+ /**
+ * @see ./edit.js
+ */
+ edit: Edit,
+} );
diff --git a/src/timeline/index.php b/src/timeline/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/index.php
diff --git a/src/timeline/render.php b/src/timeline/render.php
new file mode 100644
index 0000000..52aded4
--- /dev/null
+++ b/src/timeline/render.php
@@ -0,0 +1,8 @@
+<?php
+/**
+ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
+ */
+?>
+<p <?php echo get_block_wrapper_attributes(); ?>>
+ <?php esc_html_e( 'Menu – hello from a dynamic block!', 'menu' ); ?>
+</p>
diff --git a/src/timeline/style.scss b/src/timeline/style.scss
new file mode 100644
index 0000000..ea20261
--- /dev/null
+++ b/src/timeline/style.scss
@@ -0,0 +1,135 @@
+main {
+ --gap: 0;
+ section:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+#at-a-glance {
+ padding: 0 10vw;
+ --gap: 0;
+ img {
+ width: 100%;
+ height: auto;
+ border: 2px solid var(--action-0);
+ }
+ h3 {
+ font-size: var(--txt-x-small);
+ }
+ .before {
+ img {
+ border-right-width: 1px;
+ border-left: 0;
+ border-top: 0;
+ }
+ }
+ .after {
+ img {
+ border-left-width: 1px;
+ border-right: 0;
+ border-bottom: 0;
+ }
+ }
+}
+
+.timeline-point.timeline-point {
+ --lineWidth: 1px;
+ --gap: 2rem;
+ padding: 0;
+ margin:0;
+ background-color: var(--base);
+ max-width: 100vw;
+ position: relative;
+ overflow: hidden;
+ img {
+ width: 40%;
+ border-radius: 4px;
+ position: sticky;
+ padding: .5rem;
+ }
+ .info {
+ padding: 1rem .5rem .5rem;
+ width: 60%;
+ position: relative;
+ h2 {
+ margin: 0 0 .5rem;
+ font-size: var(--txt-medium);
+ position: relative;
+ .icon {
+ --w: 2.5rem;
+ transform: rotate(-90deg);
+ position: absolute;
+ left: -2.5rem;
+ top: .25rem;
+ background-color: var(--action-100);
+ }
+ }
+
+ }
+ &::before,
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ left: 45%;
+ height: 100%;
+ width: var(--lineWidth);
+ background-color: var(--action-0);
+ //box-shadow: var(--action-shadow);
+ }
+ &::before {
+ height: 1rem;
+ }
+ &::after {
+ top: 4rem;
+ }
+ &#before-treatment::before,
+ &:last-of-type::after {
+ display: none;
+ }
+}
+@media (min-width:768px) {
+ #at-a-glance {
+ h3 {
+ font-size: var(--txt-x-large);
+ }
+ }
+ .timeline-point.timeline-point {
+ --gap: 4rem;
+ img {
+ width: 50%;
+ }
+ .info {
+ width: 50%;
+ padding: 25vh 1rem 1rem;
+ h2 {
+ .icon {
+ --w: 4rem;
+ left: -6.15rem;
+ top: 0;
+ }
+ }
+ a {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ time {
+ text-transform: uppercase;
+ font-size: var(--txt-x-small);
+ }
+ }
+ &::before,
+ &::after {
+ left: calc(50% + 2rem);
+ }
+
+ &::before {
+ height: calc(25vh - 2rem);
+ }
+ &::after {
+ top: calc(25vh + 6rem);
+ }
+ }
+}
diff --git a/src/timeline/view.js b/src/timeline/view.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/timeline/view.js
diff --git a/src/video/block.json b/src/video/block.json
new file mode 100644
index 0000000..29149bf
--- /dev/null
+++ b/src/video/block.json
@@ -0,0 +1,79 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jvb/video",
+ "version": "1.0.0",
+ "title": "Video Cover",
+ "category": "jvb",
+ "icon": "video-alt3",
+ "description": "Self-hosted video cover with poster and multiple format support",
+ "supports": {
+ "html": false,
+ "align": ["wide", "full"],
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "color": {
+ "background": true,
+ "text": true
+ }
+ },
+ "attributes": {
+ "title": {
+ "type": "string",
+ "default": ""
+ },
+ "description": {
+ "type": "rich-text",
+ "default": ""
+ },
+ "posterId": {
+ "type": "number",
+ "default": 0
+ },
+ "posterUrl": {
+ "type": "string",
+ "default": ""
+ },
+ "videoSources": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "url": {
+ "type": "string"
+ },
+ "mime": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "fadeEffect": {
+ "type": "boolean",
+ "default": false
+ },
+ "overlayOpacity": {
+ "type": "number",
+ "default": 0
+ },
+ "contentAlignment": {
+ "type": "string",
+ "default": "center"
+ },
+ "minHeight": {
+ "type": "number",
+ "default": 0
+ }
+ },
+ "textdomain": "jvb",
+ "editorScript": "file:./index.js",
+ "viewScript": "file:./view.js",
+ "editorStyle": "file:./index.css",
+ "style": "file:./style-index.css"
+}
diff --git a/src/video/edit.js b/src/video/edit.js
new file mode 100644
index 0000000..9830e65
--- /dev/null
+++ b/src/video/edit.js
@@ -0,0 +1,276 @@
+//edit.js
+import { __ } from '@wordpress/i18n';
+import {
+ useBlockProps,
+ InspectorControls,
+ MediaUpload,
+ MediaUploadCheck,
+ InnerBlocks,
+ useInnerBlocksProps
+} from '@wordpress/block-editor';
+import {
+ PanelBody,
+ Button,
+ ToggleControl,
+ BaseControl,
+ RangeControl,
+ SelectControl
+} from '@wordpress/components';
+import './editor.scss';
+
+const ALLOWED_VIDEO_TYPES = ['video'];
+
+const INNER_BLOCKS_TEMPLATE = [
+ ['core/heading', {
+ level: 1,
+ placeholder: 'Add heading...',
+ textAlign: 'center'
+ }],
+ ['core/paragraph', {
+ placeholder: 'Add description...',
+ align: 'center'
+ }],
+ ['core/buttons', {
+ layout: { type: 'flex', justifyContent: 'center' }
+ }]
+];
+
+
+
+export default function Edit({ attributes, setAttributes }) {
+ const {
+ posterId,
+ posterUrl,
+ videoSources,
+ fadeEffect,
+ overlayOpacity,
+ contentAlignment,
+ minHeight
+ } = attributes;
+
+ const blockProps = useBlockProps({
+ className: 'video-cover-editor',
+ style: {
+ minHeight: minHeight ? `${minHeight}px` : undefined
+ }
+ });
+
+ const innerBlocksProps = useInnerBlocksProps(
+ { className: 'video-cover-content' },
+ {
+ template: INNER_BLOCKS_TEMPLATE,
+ templateLock: false
+ }
+ );
+
+ const onSelectPoster = (media) => {
+ setAttributes({
+ posterId: media.id,
+ posterUrl: media.url
+ });
+ };
+
+ const onSelectVideos = (mediaItems) => {
+ // multiple=true returns an array
+ const items = Array.isArray(mediaItems) ? mediaItems : [mediaItems];
+ const newSources = items
+ .filter(media => !videoSources.some(s => s.id === media.id))
+ .map(media => ({
+ id: media.id,
+ url: media.url,
+ mime: media.mime
+ }));
+
+ if (newSources.length) {
+ setAttributes({
+ videoSources: [...videoSources, ...newSources]
+ });
+ }
+ };
+
+ const removeVideoSource = (index) => {
+ const updated = [...videoSources];
+ updated.splice(index, 1);
+ setAttributes({ videoSources: updated });
+ };
+
+ const renderVideoSourceList = (sources, isMobile = false) => {
+ if (sources.length === 0) return null;
+
+ return (
+ <ul className="video-source-list">
+ {sources.map((source, index) => (
+ <li key={index} className="video-source-item">
+ <span className="video-source-mime">{source.mime}</span>
+ <Button
+ isDestructive
+ isSmall
+ onClick={() => removeVideoSource(index, isMobile)}
+ >
+ {__('Remove', 'jvb')}
+ </Button>
+ </li>
+ ))}
+ </ul>
+ );
+ };
+
+ return (
+ <>
+ <InspectorControls>
+ <PanelBody title={__('Video Settings', 'jvb')} initialOpen={true}>
+ <BaseControl
+ label={__('Poster Image', 'jvb')}
+ help={__('Image shown while video loads', 'jvb')}
+ >
+ <MediaUploadCheck>
+ <MediaUpload
+ onSelect={onSelectPoster}
+ allowedTypes={['image']}
+ value={posterId}
+ render={({ open }) => (
+ <>
+ {posterUrl && (
+ <img
+ src={posterUrl}
+ alt={__('Poster preview', 'jvb')}
+ style={{ maxWidth: '100%', marginBottom: '10px' }}
+ />
+ )}
+ <Button
+ onClick={open}
+ variant={posterUrl ? 'secondary' : 'primary'}
+ >
+ {posterUrl
+ ? __('Change Poster', 'jvb')
+ : __('Select Poster', 'jvb')}
+ </Button>
+ {posterUrl && (
+ <Button
+ isDestructive
+ onClick={() => setAttributes({ posterId: 0, posterUrl: '' })}
+ style={{ marginLeft: '10px' }}
+ >
+ {__('Remove', 'jvb')}
+ </Button>
+ )}
+ </>
+ )}
+ />
+ </MediaUploadCheck>
+ </BaseControl>
+
+ <BaseControl
+ label={__('Video Sources', 'jvb')}
+ help={__('Add multiple formats for better browser support (mp4, webm, etc.)', 'jvb')}
+ >
+ {videoSources.length > 0 && (
+ <ul className="video-source-list">
+ {videoSources.map((source, index) => (
+ <li key={index} className="video-source-item">
+ <span className="video-source-mime">{source.mime}</span>
+ <Button
+ isDestructive
+ isSmall
+ onClick={() => removeVideoSource(index)}
+ >
+ {__('Remove', 'jvb')}
+ </Button>
+ </li>
+ ))}
+ </ul>
+ )}
+ <MediaUploadCheck>
+ <MediaUpload
+ multiple={true}
+ onSelect={onSelectVideos}
+ allowedTypes={ALLOWED_VIDEO_TYPES}
+ render={({ open }) => (
+ <Button onClick={open} variant="secondary">
+ {__('Add Video', 'jvb')}
+ </Button>
+ )}
+ />
+ </MediaUploadCheck>
+ </BaseControl>
+
+ <ToggleControl
+ label={__('Fade Effect', 'jvb')}
+ help={__('Add fade class to video element', 'jvb')}
+ checked={fadeEffect}
+ onChange={(value) => setAttributes({ fadeEffect: value })}
+ />
+ </PanelBody>
+
+ <PanelBody title={__('Overlay Settings', 'jvb')} initialOpen={true}>
+ <RangeControl
+ label={__('Overlay Opacity', 'jvb')}
+ help={__('Darken video for better text readability', 'jvb')}
+ value={overlayOpacity}
+ onChange={(value) => setAttributes({ overlayOpacity: value })}
+ min={0}
+ max={100}
+ step={5}
+ />
+
+ <SelectControl
+ label={__('Content Alignment', 'jvb')}
+ value={contentAlignment}
+ options={[
+ { label: __('Top Left', 'jvb'), value: 'top-left' },
+ { label: __('Top Center', 'jvb'), value: 'top-center' },
+ { label: __('Top Right', 'jvb'), value: 'top-right' },
+ { label: __('Center Left', 'jvb'), value: 'center-left' },
+ { label: __('Center', 'jvb'), value: 'center' },
+ { label: __('Center Right', 'jvb'), value: 'center-right' },
+ { label: __('Bottom Left', 'jvb'), value: 'bottom-left' },
+ { label: __('Bottom Center', 'jvb'), value: 'bottom-center' },
+ { label: __('Bottom Right', 'jvb'), value: 'bottom-right' }
+ ]}
+ onChange={(value) => setAttributes({ contentAlignment: value })}
+ />
+
+ <RangeControl
+ label={__('Minimum Height', 'jvb')}
+ help={__('Minimum height in pixels (leave 0 for auto)', 'jvb')}
+ value={minHeight}
+ onChange={(value) => setAttributes({ minHeight: value })}
+ min={0}
+ max={1000}
+ step={50}
+ />
+ </PanelBody>
+ </InspectorControls>
+
+ <div {...blockProps}>
+ {posterUrl || videoSources.length > 0 ? (
+ <div className="video-cover-preview">
+ {posterUrl && (
+ <>
+ <img src={posterUrl} alt={__('Video poster', 'jvb')} />
+ {overlayOpacity > 0 && (
+ <div
+ className="video-overlay-preview"
+ style={{ opacity: overlayOpacity / 100 }}
+ />
+ )}
+ </>
+ )}
+ <div className={`video-cover-content-preview align-${contentAlignment}`}>
+ <div {...innerBlocksProps} />
+ </div>
+ <div className="video-info">
+ <p>
+ {videoSources.length} {__('desktop source(s)', 'jvb')}
+ </p>
+ </div>
+ </div>
+ ) : (
+ <div className="video-cover-placeholder">
+ <p>{__('Configure video sources in the sidebar →', 'jvb')}</p>
+ </div>
+ )}
+ </div>
+ </>
+ );
+}
diff --git a/src/video/editor.scss b/src/video/editor.scss
new file mode 100644
index 0000000..afc9f13
--- /dev/null
+++ b/src/video/editor.scss
@@ -0,0 +1,141 @@
+/* editor.scss */
+.video-cover-editor {
+ position: relative;
+ min-height: 200px;
+ background: #f0f0f0;
+ border: 2px dashed #ccc;
+ border-radius: 4px;
+
+ .video-cover-preview {
+ position: relative;
+ width: 100%;
+ min-height: 300px;
+
+ img {
+ width: 100%;
+ height: auto;
+ display: block;
+ }
+
+ .video-overlay-preview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 1);
+ pointer-events: none;
+ }
+
+ .video-cover-content-preview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ z-index: 2;
+ padding: 2rem;
+
+ // Content alignment classes
+ &.align-top-left {
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+ &.align-top-center {
+ align-items: flex-start;
+ justify-content: center;
+ }
+ &.align-top-right {
+ align-items: flex-start;
+ justify-content: flex-end;
+ }
+ &.align-center-left {
+ align-items: center;
+ justify-content: flex-start;
+ }
+ &.align-center {
+ align-items: center;
+ justify-content: center;
+ }
+ &.align-center-right {
+ align-items: center;
+ justify-content: flex-end;
+ }
+ &.align-bottom-left {
+ align-items: flex-end;
+ justify-content: flex-start;
+ }
+ &.align-bottom-center {
+ align-items: flex-end;
+ justify-content: center;
+ }
+ &.align-bottom-right {
+ align-items: flex-end;
+ justify-content: flex-end;
+ }
+
+ .video-cover-content {
+ width: 100%;
+ max-width: 1200px;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+
+ // Style inner blocks for better visibility in editor
+ h1, h2, h3, h4, h5, h6 {
+ color: white;
+ }
+
+ p {
+ color: white;
+ }
+ }
+ }
+
+ .video-info {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 10px;
+ font-size: 14px;
+ z-index: 3;
+
+ p {
+ margin: 0;
+ }
+ }
+ }
+
+ .video-cover-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ color: #666;
+ font-size: 16px;
+ }
+}
+
+.video-source-list {
+ list-style: none;
+ margin: 10px 0;
+ padding: 0;
+
+ .video-source-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 12px;
+ background: #f5f5f5;
+ border-radius: 4px;
+ margin-bottom: 5px;
+
+ .video-source-mime {
+ font-family: monospace;
+ font-size: 13px;
+ }
+ }
+}
diff --git a/src/video/index.js b/src/video/index.js
new file mode 100644
index 0000000..2f9cf8b
--- /dev/null
+++ b/src/video/index.js
@@ -0,0 +1,21 @@
+/* index.js */
+import { registerBlockType } from '@wordpress/blocks';
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+import './style.scss';
+import Edit from './edit';
+import metadata from './block.json';
+
+registerBlockType(metadata.name, {
+ edit: Edit,
+ save: ({ attributes }) => {
+ const blockProps = useBlockProps.save({
+ className: 'video-cover-wrapper-placeholder'
+ });
+
+ return (
+ <div {...blockProps}>
+ <InnerBlocks.Content />
+ </div>
+ );
+ }
+});
diff --git a/src/video/index.php b/src/video/index.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/video/index.php
diff --git a/src/video/render.php b/src/video/render.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/src/video/render.php
@@ -0,0 +1 @@
+<?php
diff --git a/src/video/save.js b/src/video/save.js
new file mode 100644
index 0000000..da66ed1
--- /dev/null
+++ b/src/video/save.js
@@ -0,0 +1,23 @@
+/**
+ * save.js
+ * React hook that is used to mark the block wrapper element.
+ * It provides all the necessary props like the class name.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * The save function defines the way in which the different attributes should
+ * be combined into the final markup, which is then serialized by the block
+ * editor into `post_content`.
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
+ *
+ * @return {WPElement} Element to render.
+ */
+export default function save() {
+ // This is a dynamic block that is rendered on the server side
+ // Return null to let WordPress handle the saving and rendering
+ return null;
+}
diff --git a/src/video/style.scss b/src/video/style.scss
new file mode 100644
index 0000000..d16e7d9
--- /dev/null
+++ b/src/video/style.scss
@@ -0,0 +1,178 @@
+/** style.scss **/
+.video-cover {
+ position: relative;
+ width: 100%;
+ min-height: 75vh;
+ overflow: hidden;
+ display: flex;
+ .wrap {
+ background-color: var(--contrast-200);
+ //&::before {
+ // position: absolute;
+ // top: 0;
+ // bottom: 0;
+ // left: 0;
+ // right: 0;
+ // background-color: var(--base);
+ // mix-blend-mode: lighten;
+ // content: '';
+ // z-index: 1;
+ //}
+ }
+ /* Video background */
+ .video-container {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ min-width: 100%;
+ min-height: 100%;
+ z-index: 0;
+ display: flex;
+ background-color: var(--action-50);
+
+ &.fade {
+ animation: fadeIn 1s ease-in;
+ }
+
+ video {
+ pointer-events: none;
+ opacity: .85;
+ mix-blend-mode: multiply;
+ filter: grayscale(100%) contrast(1);
+ flex: 1 0 100%;
+ object-fit: cover;
+ }
+ }
+
+ .inner-wrap {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ padding: 2rem;
+ color: var(--action-contrast);
+
+ /* Better text readability */
+ h1, h2, h3, h4, h5, h6 {
+ word-spacing: 100vw;
+ color: var(--action-contrast);
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ margin: 2rem 0 0;
+ }
+
+ p {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin: 0;
+ color: var(--action-contrast);
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+ }
+
+ .media-text {
+ }
+ .media-text figure {
+ max-width: 50%;
+ }
+ @media (min-width: 768px) {
+ .media-text {
+ --align: flex-start;
+ gap: 3rem;
+ max-width: var(--content);
+ }
+ }
+ .media-text > div {
+ width: fit-content;
+ }
+ .buttons a {
+ font-weight: var(--fw-h-bold);
+ color: var(--action-contrast);
+ border-color: var(--action-contrast);
+ &:visited {
+ color: var(--action-contrast);
+ &:hover {
+ color: var(--action-contrast);
+ }
+ }
+ &:hover {
+ background-color: var(--action-0);
+ color: var(--action-contrast);
+ }
+ }
+
+ .outline a {
+ background-color: rgba(var(--base-rgb), rgba(var(--base-rgb),var(--op-3)));
+ }
+ .buttons {
+ margin: 3rem 0;
+ li {
+ background-color: rgba(var(--action-rgb), var(--op-4));
+ }
+ }
+ /* Button styles */
+ .wp-block-button__link {
+ text-shadow: none;
+ }
+ }
+
+ /* Alignment classes */
+ &.align-top-left {
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+ &.align-top-center {
+ align-items: flex-start;
+ justify-content: center;
+ }
+ &.align-top-right {
+ align-items: flex-start;
+ justify-content: flex-end;
+ }
+ &.align-center-left {
+ align-items: center;
+ justify-content: flex-start;
+ }
+ &.align-center {
+ align-items: center;
+ justify-content: center;
+ }
+ &.align-center-right {
+ align-items: center;
+ justify-content: flex-end;
+ }
+ &.align-bottom-left {
+ align-items: flex-end;
+ justify-content: flex-start;
+ }
+ &.align-bottom-center {
+ align-items: flex-end;
+ justify-content: center;
+ }
+ &.align-bottom-right {
+ align-items: flex-end;
+ justify-content: flex-end;
+ }
+
+ /* Full-width alignment */
+ &.alignfull {
+ width: 100vw;
+ max-width: none;
+ margin-left: calc(50% - 50vw);
+ margin-right: calc(50% - 50vw);
+ }
+
+ /* Wide alignment */
+ &.alignwide {
+ max-width: 1200px;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
diff --git a/src/video/view.js b/src/video/view.js
new file mode 100644
index 0000000..76bcd1a
--- /dev/null
+++ b/src/video/view.js
@@ -0,0 +1,47 @@
+/** view.js **/
+document.addEventListener("DOMContentLoaded", function () {
+ const lazyVideos = [].slice.call(
+ document.querySelectorAll(".video-container video")
+ );
+
+ // Build a helper to actually set sources + load
+ function loadVideo(video) {
+ const sources = video.querySelectorAll("source[data-src]");
+ sources.forEach(source => {
+ source.src = source.dataset.src;
+ });
+ video.load();
+ }
+
+ // --- 1. IntersectionObserver (best case) ---
+ if ("IntersectionObserver" in window) {
+ const lazyVideoObserver = new IntersectionObserver(
+ function (entries, observer) {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ loadVideo(entry.target);
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ {
+ rootMargin: "200px 0px",
+ threshold: 0.1,
+ }
+ );
+
+ lazyVideos.forEach(video => lazyVideoObserver.observe(video));
+ return;
+ }
+
+ // --- 2. Fallback: requestIdleCallback ---
+ if ("requestIdleCallback" in window) {
+ requestIdleCallback(() => {
+ lazyVideos.forEach(video => loadVideo(video));
+ });
+ return;
+ }
+
+ // --- 3. Final fallback: load immediately ---
+ lazyVideos.forEach(video => loadVideo(video));
+});
diff --git a/templates/dashboard/sections/news.php b/templates/dashboard/sections/news.php
index 29e7757..47e1148 100644
--- a/templates/dashboard/sections/news.php
+++ b/templates/dashboard/sections/news.php
@@ -118,7 +118,7 @@
</div>
<details class="type-filters">
- <summary class="row btw">Filters:
+ <summary class="row x-btw">Filters:
<button class="clear-filters row">
<?= jvbIcon('x', ['title' => 'Clear Filters'])?>
<span>Clear Filters</span>
@@ -274,7 +274,7 @@
</dialog>
<template class="template-own">
<details class="news item" data-keyboard-nav="true" tabindex="0">
- <summary class="row btw">
+ <summary class="row x-btw">
<div class="item-select">
<input type="checkbox" class="select-checkbox">
<label>
@@ -317,7 +317,7 @@
</template>
<template class="template-all template-watching">
<details class="news item" data-keyboard-nav="true" tabindex="0">
- <summary class="row btw">
+ <summary class="row x-btw">
<button class="favourite-button" data-type="news" title="Add to watch list" onclick="toggleFavourite(this)">
</button>
diff --git a/templates/dashboard/sections/notifications.php b/templates/dashboard/sections/notifications.php
index 8969360..18b672a 100644
--- a/templates/dashboard/sections/notifications.php
+++ b/templates/dashboard/sections/notifications.php
@@ -29,7 +29,7 @@
?>
<div class="container">
- <nav class="tabs row start" role="tablist">
+ <nav class="tabs row left" role="tablist">
<?php
$i =0;
diff --git a/webpack.jvb.js b/webpack.jvb.js
index 7995231..38b5d04 100644
--- a/webpack.jvb.js
+++ b/webpack.jvb.js
@@ -36,13 +36,13 @@
'quill': './assets/js/concise/quill.js',
'queue': './assets/js/concise/Queue.js',
'referral': './assets/js/concise/Referral.js',
- 'referralAdmin': './assets/js/concise/ReferralAdmin.js',
+ 'referralAdmin': './assets/js/concise/ReferralAdmin.js',
'shopManager': './assets/js/concise/ShopManager.js',
'cache': './assets/js/concise/SimpleCache.js',
'schema': './assets/js/concise/SchemaManager.js',
'square': './assets/js/concise/CheckoutSquare.js',
'helcim': './assets/js/concise/CheckoutHelcim.js',
- 'checkout': './assets/js/concise/Checkout.js',
+ 'checkout': './assets/js/concise/Checkout.js',
'tabs': './assets/js/concise/Tabs.js',
'creator': './assets/js/concise/TaxonomyCreator.js',
'selector': './assets/js/concise/TaxonomySelector.js',
--
Gitblit v1.10.0