mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor blog carousel to new data attribute structure
This commit is contained in:
1 parent
a45b4934c6
commit
44431ce944
8 files changed
+287
-535
No files matched your search
@@ -1,3 +1,3 @@
|
||||
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-logo-accent:var(--color-primary);--color-logo-wordmark:var(--color-neutral-700);--color-logo-wordmark-inverse:var(--color-white);--color-logo-background:var(--color-neutral-700);--color-logo-background-inverse:var(--color-white);--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-info-glass:rgba(37,99,235,.18);--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-text-muted:var(--color-muted);--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-warning:#facc15;--color-warning-strong:#d97706;--color-warning-surface:rgba(146,64,14,.24);--color-warning-glass:rgba(250,204,21,.18);--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--color-overlay-black:rgba(0,0,0,.9);--color-overlay-muted:rgba(15,23,42,.55);--color-overlay-strong:rgba(15,23,42,.75);--color-overlay-darker:rgba(15,23,42,.88);--color-overlay-panel:rgba(26,34,52,.82);--color-overlay-panel-soft:rgba(26,34,52,.65);--color-overlay-panel-tint:rgba(31,41,55,.5);--color-overlay-panel-glass:rgba(28,37,58,.72);--color-overlay-panel-glass-soft:rgba(31,41,55,.55);--color-modal-backdrop:rgba(8,12,24,.58);--color-modal-surface:rgba(20,28,46,.78);--color-overlay-veil:rgba(11,17,32,.96);--color-overlay-banner:rgba(51,65,85,.95);--color-sidebar-surface:rgba(17,24,39,.94);--color-sidebar-surface-alt:rgba(11,22,48,.78);--color-sidebar-border:rgba(148,163,184,.28);--color-sidebar-border-strong:rgba(148,163,184,.42);--color-sidebar-hover:rgba(56,189,248,.18);--color-sidebar-hover-strong:rgba(129,140,248,.28);--color-sidebar-glow:rgba(56,189,248,.35);--color-sidebar-accent-start:rgba(56,189,248,.9);--color-sidebar-accent-end:rgba(129,140,248,.85);--color-sidebar-text-muted:rgba(203,213,225,.75);--color-overlay-panel-gradient-start:rgba(30,41,59,.92);--color-overlay-panel-gradient-end:rgba(15,23,42,.92);--color-surface-tint-subtle:rgba(148,163,184,.08);--color-border-translucent:rgba(148,163,184,.18);--color-border-translucent-strong:rgba(148,163,184,.25);--color-border-translucent-bright:rgba(148,163,184,.32);--color-border-translucent-stronger:rgba(148,163,184,.35);--color-border-translucent-strongest:rgba(148,163,184,.4);--color-border-translucent-glow:rgba(148,163,184,.55);--color-border-translucent-medium:rgba(148,163,184,.2);--color-border-neutral-soft:rgba(229,231,235,.35);--color-border-info-soft:rgba(96,165,250,.35);--color-border-sidebar-strong:rgba(55,65,81,.9);--color-ring-private:rgba(168,85,247,.55);--color-ring-private-hover:rgba(192,132,252,.65);--color-ring-critical:rgba(239,68,68,.4);--color-ring-critical-soft:rgba(239,68,68,.35);--color-ring-critical-hover:rgba(239,68,68,.45);--color-ring-critical-strong:rgba(239,68,68,.55);--color-ring-alert:rgba(220,38,38,.6);--color-gradient-visibility-start:rgba(129,140,248,.4);--color-gradient-visibility-end:rgba(192,132,252,.32);--color-white-soft:hsla(0,0%,100%,.1);--color-white-softer:hsla(0,0%,100%,.3);--color-white-softest:hsla(0,0%,100%,.4);--color-shadow-floating:rgba(8,11,30,.55);--color-shadow-elevated:rgba(8,11,30,.45);--color-shadow-panel:rgba(15,23,42,.35);--color-shadow-notification:rgba(15,23,42,.3);--color-shadow-sidebar:rgba(0,0,0,.45);--color-divider-strong:rgba(15,23,42,.35);--color-text-frosted-strong:rgba(226,232,240,.94);--color-text-frosted:rgba(226,232,240,.9);--color-text-frosted-soft:rgba(226,232,240,.82);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--sidebar-width-expanded:16rem;--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-lg);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-Line truncated
|
||||
|
||||
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.ng-animate-shim{visibility:hidden}.ng-anchor{position:absolute}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-md);padding-bottom:var(--space-md)}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transitionLine truncated
|
||||
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:100%}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) transparent}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar,body.modal-open #sidebarOverlay{pointer-events:none;opacity:0;visibility:hidden}body.modal-open #sidebar{z-index:var(--z-sidebar-overlay)}body.modal-open #sidebarOverlay{z-index:calc(var(--z-sidebar-overlay) - 1)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.ng-animate-shim{visibility:hidden}.ng-anchor{position:absolute}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:flex}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-md);padding-bottom:var(--space-md)}.focus-ring{outline:2px solid transparent;outline-offset:2px}.focus-ring:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--color-info);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--color-surface);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transitionLine truncated
|
||||
+36
-444
@@ -336,6 +336,7 @@
|
||||
--blog-carousel-track-ease: var(--motion-ease-out, ease-out);
|
||||
--blog-carousel-slide-duration: var(--motion-duration-base, 160ms);
|
||||
--blog-carousel-progress-duration: var(--motion-duration-slow, 320ms);
|
||||
--blog-carousel-per-page: 1;
|
||||
}
|
||||
|
||||
.blog-carousel[data-transition="instant"] {
|
||||
@@ -344,6 +345,7 @@
|
||||
}
|
||||
|
||||
.blog-carousel__viewport {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--blog-carousel-slide-radius);
|
||||
}
|
||||
@@ -351,7 +353,7 @@
|
||||
.blog-carousel__track {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(0, 100%);
|
||||
grid-auto-columns: minmax(0, calc(100% / var(--blog-carousel-per-page)));
|
||||
gap: var(--blog-carousel-gap);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -439,54 +441,37 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blog-carousel[data-per-page="1"] {
|
||||
--blog-carousel-per-page: 1;
|
||||
}
|
||||
|
||||
.blog-carousel[data-per-page="2"] {
|
||||
--blog-carousel-per-page: 2;
|
||||
}
|
||||
|
||||
.blog-carousel[data-per-page="3"] {
|
||||
--blog-carousel-per-page: 3;
|
||||
}
|
||||
|
||||
.blog-carousel[data-per-page="4"] {
|
||||
--blog-carousel-per-page: 4;
|
||||
}
|
||||
|
||||
.blog-carousel__controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.blog-progress {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
--blog-carousel-progress-duration: var(--motion-duration-slow, 320ms);
|
||||
}
|
||||
|
||||
.blog-progress[data-transition="instant"] {
|
||||
--blog-carousel-progress-duration: 0s;
|
||||
}
|
||||
|
||||
.blog-progress__item {
|
||||
position: relative;
|
||||
flex: 1 1 0%;
|
||||
height: var(--blog-carousel-progress-height);
|
||||
border-radius: var(--blog-carousel-progress-radius);
|
||||
background: var(--blog-carousel-progress-track);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blog-progress__item::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--blog-carousel-progress-active);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: transform var(--blog-carousel-progress-duration)
|
||||
var(--motion-ease-out, ease-out);
|
||||
}
|
||||
|
||||
.blog-progress[data-state="paused"] .blog-progress__item::after {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.blog-progress__item[data-active="true"]::after,
|
||||
.blog-progress__item[data-state="complete"]::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.blog-progress__item[data-active="true"] {
|
||||
background: var(--blog-carousel-progress-track, var(--color-border));
|
||||
}
|
||||
|
||||
.blog-progress__item[data-state="upcoming"]::after {
|
||||
transform: scaleX(0);
|
||||
.blog-progress [data-progress-meter] {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -504,9 +489,6 @@
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.blog-progress__item::after {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-emoji-showdown {
|
||||
@@ -2935,44 +2917,21 @@
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__slide {
|
||||
.blog-app-root .short-notes .blog-carousel__slide {
|
||||
font-size: var(--blog-font-size-body);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__slide a {
|
||||
.blog-app-root .short-notes .blog-carousel__slide a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__slide .date {
|
||||
.blog-app-root .short-notes .blog-carousel__slide .date {
|
||||
font-size: var(--blog-font-size-meta);
|
||||
color: var(--color-blog-gray-275);
|
||||
margin-bottom: var(--blog-space-compact);
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__arrow--prev {
|
||||
left: -3em;
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__arrow--next {
|
||||
right: -3em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.blog-app-root .short-notes .splide__arrow--prev,
|
||||
.blog-app-root .short-notes .splide__arrow--next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__arrow {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.blog-app-root .short-notes .splide__arrow svg {
|
||||
fill: var(--color-blog-gray-275);
|
||||
}
|
||||
|
||||
.dark .blog-app-root .short-notes {
|
||||
border-bottom: var(--space-xs) solid var(--color-card);
|
||||
}
|
||||
@@ -2981,6 +2940,12 @@
|
||||
color: var(--color-blog-gray-220);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.blog-app-root .short-notes [data-carousel-control] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-app-root .listing-notes {
|
||||
margin-top: var(--space-md);
|
||||
color: var(--color-blog-gray-440);
|
||||
@@ -3544,379 +3509,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.blog-app-root .splide__container {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__list {
|
||||
backface-visibility: hidden;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-initialized:not(.is-active) .splide__list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination li {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.blog-app-root .splide:not(.is-overflow) .splide__pagination {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__progress {
|
||||
border-radius: var(--blog-carousel-progress-radius);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__progress__bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.blog-app-root .splide {
|
||||
position: relative;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-initialized,
|
||||
.blog-app-root .splide.is-rendered {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__slide {
|
||||
backface-visibility: hidden;
|
||||
box-sizing: border-box;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
list-style-type: none !important;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__slide img {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__spinner {
|
||||
animation: splide-loading 1s linear infinite;
|
||||
border: 2px solid var(--color-blog-gray-300);
|
||||
border-left-color: transparent;
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
contain: strict;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__sr {
|
||||
clip: rect(0 0 0 0);
|
||||
border: 0;
|
||||
height: var(--space-4xs);
|
||||
margin: calc(var(--space-4xs) * -1);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: var(--space-4xs);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__toggle.is-active .splide__toggle__play,
|
||||
.blog-app-root .splide__toggle__pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__toggle.is-active .splide__toggle__pause {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes splide-loading {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--draggable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--fade > .splide__list > .splide__slide {
|
||||
margin: 0 !important;
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--fade > .splide__list > .splide__slide.is-active {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blog-app-root .splide--rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--ttb > .splide__list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background: var(--color-blog-gray-210);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
height: 2em;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 2em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow svg {
|
||||
fill: var(--color-black);
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow:focus-visible {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow--prev {
|
||||
left: 1em;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow--prev svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrow--next {
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-focus-in .splide__arrow:focus {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination {
|
||||
bottom: 0.5em;
|
||||
left: 0;
|
||||
padding: 0 1em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination__page {
|
||||
background: var(--color-blog-gray-210);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin: var(--outline-thick-width);
|
||||
opacity: 0.7;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
transition: transform 0.2s linear;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination__page.is-active {
|
||||
background: var(--color-white);
|
||||
transform: scale(1.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination__page:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination__page:focus-visible {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-focus-in .splide__pagination__page:focus {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__progress__bar {
|
||||
height: var(--blog-carousel-progress-height);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__slide {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__slide:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@supports (outline-offset: calc(var(--outline-thick-width) * -1)) {
|
||||
.blog-app-root .splide__slide:focus-visible {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: calc(var(--outline-thick-width) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: none) {
|
||||
.blog-app-root .splide__slide:focus-visible {
|
||||
border: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (outline-offset: calc(var(--outline-thick-width) * -1)) {
|
||||
.blog-app-root .splide.is-focus-in .splide__slide:focus {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: calc(var(--outline-thick-width) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: none) {
|
||||
.blog-app-root .splide.is-focus-in .splide__slide:focus {
|
||||
border: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-focus-in .splide__track > .splide__list > .splide__slide:focus {
|
||||
border-color: var(--color-blog-sky-500);
|
||||
}
|
||||
}
|
||||
|
||||
.blog-app-root .splide__toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__toggle:focus-visible {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide.is-focus-in .splide__toggle:focus {
|
||||
outline: var(--outline-thick-width) solid var(--color-blog-sky-500);
|
||||
outline-offset: var(--outline-thick-width);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--nav > .splide__list > .splide__slide {
|
||||
border: var(--outline-thick-width) solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__track--nav > .splide__list > .splide__slide.is-active {
|
||||
border: var(--outline-thick-width) solid var(--color-black);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--rtl .splide__arrow--prev {
|
||||
left: auto;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--rtl .splide__arrow--prev svg {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--rtl .splide__arrow--next {
|
||||
left: 1em;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--rtl .splide__arrow--next svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--ttb .splide__arrow {
|
||||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--ttb .splide__arrow--prev {
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--ttb .splide__arrow--prev svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--ttb .splide__arrow--next {
|
||||
bottom: 1em;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.blog-app-root .splide__arrows--ttb .splide__arrow--next svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.blog-app-root .splide__pagination--ttb {
|
||||
bottom: 0;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
left: auto;
|
||||
padding: 1em 0;
|
||||
right: 0.5em;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.blog-app-root .loader-container.svelte-181fgen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -43,6 +43,40 @@ The blog embed now consumes dedicated typography and spacing tokens alongside Ta
|
||||
|
||||
Templates should reference these tokens (directly or via Tailwind arbitrary values) instead of hard-coded measurements so the embed stays in sync with future blog refreshes.
|
||||
|
||||
### Blog carousel markup
|
||||
|
||||
The short-notes carousel ships as a semantic wrapper around the `blog-carousel` primitive defined in `css/tailwind.source.css`. The runtime no longer injects inline styles or Splide classes—layout is driven entirely by data attributes that map onto tokenised CSS rules.
|
||||
|
||||
**Structure**
|
||||
|
||||
```html
|
||||
<section class="short-notes blog-carousel" data-carousel data-per-page="3" data-index="0">
|
||||
<div class="blog-carousel__viewport" data-carousel-viewport>
|
||||
<ul class="blog-carousel__track" data-carousel-track>
|
||||
<li class="blog-carousel__slide" data-carousel-slide data-index="0" data-active="true">
|
||||
...
|
||||
</li>
|
||||
<!-- additional slides -->
|
||||
</ul>
|
||||
<button type="button" data-carousel-prev data-carousel-control="prev">‹</button>
|
||||
<button type="button" data-carousel-next data-carousel-control="next">›</button>
|
||||
</div>
|
||||
<div class="blog-carousel__progress blog-progress" data-carousel-progress>
|
||||
<progress class="progress progress--blog" data-progress-meter aria-label="Carousel progress"></progress>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**State contract**
|
||||
|
||||
- `data-per-page` controls how many columns render inside the viewport. CSS exposes variants for 1–4 slides per page; the custom property `--blog-carousel-per-page` updates automatically.
|
||||
- `data-index` represents the active *page* (zero-indexed). The track translates by `100%` for each page, so the attribute should advance in whole-number increments rather than slide indices.
|
||||
- Slides expose `data-index`, `data-active`, `data-inert`, and `data-state`. The carousel controller toggles `data-state="active"`, `"idle"`, and `"leaving"` to drive scale/opacity transitions without touching inline styles.
|
||||
- The root `section` advertises `data-state="active" | "idle"` plus `data-transition="auto" | "instant"` when short-circuiting animations (for example, during first render or programmatic jumps).
|
||||
- Progress wrappers set `data-state="idle" | "active" | "paused" | "complete"` while the inner `<progress>` leverages the shared `progress` primitive (`data-progress-meter`, `data-variant="blog"`) so theme tokens continue to drive track/fill colours.
|
||||
|
||||
Controllers should rely on these data hooks when orchestrating autoplay, pausing on pointer/visibility changes, or syncing the segmented progress indicator. Avoid reintroducing inline `style` mutations—tests enforce the contract via the inline-style guard.
|
||||
|
||||
### Layout & spacing extensions
|
||||
|
||||
The core spacing scale gained intermediate stops for tighter micro-layouts and larger hero treatments. `css/tokens.css` now exports `--space-4xs`, `--space-3xs`, `--space-xs-snug`, `--space-md-plus`, `--space-xl-compact`, `--space-xl-plus`, and `--space-2xl-plus`; Tailwind surfaces matching utilities such as `p-4xs`, `p-3xs`, `px-xs-snug`, `py-md-plus`, `gap-xl-compact`, `px-xl-plus`, and `gap-2xl-plus`. Pair them with the new layout primitives—`--menu-min-width` (`min-w-menu`), `--modal-max-width` (`max-w-modal`), `--layout-player-max-width` (`max-w-player`), and `--layout-docs-max-width` (`max-w-docs`)—to anchor consistent breakpoints across popovers, modals, and documentation shells without reintroducing raw `rem`/`px` literals. Blog-specific chrome reuses the same additions: the theme toggle knob now references `--radius-toggle-thumb`, and focus affordances draw from `--outline-thick-width` instead of hard-coded pixel outlines.
|
||||
|
||||
+150
-43
@@ -1,5 +1,3 @@
|
||||
import { applyDynamicStyles } from '../ui/styleSystem.js';
|
||||
|
||||
function toArray(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
@@ -7,6 +5,85 @@ function toArray(value) {
|
||||
return Array.isArray(value) ? value : Array.from(value);
|
||||
}
|
||||
|
||||
const SLIDE_STATE_IDLE = 'idle';
|
||||
const SLIDE_STATE_ACTIVE = 'active';
|
||||
const SLIDE_STATE_LEAVING = 'leaving';
|
||||
const SLIDE_TRANSITION_TIMEOUT = 420;
|
||||
|
||||
function getRange(pageIndex, perPage) {
|
||||
const start = pageIndex * perPage;
|
||||
return { start, end: start + perPage };
|
||||
}
|
||||
|
||||
function clearTimer(state, slide) {
|
||||
const handle = state.timers.get(slide);
|
||||
if (typeof handle === 'number') {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
state.timers.delete(slide);
|
||||
}
|
||||
|
||||
function scheduleSlideIdle(state, slide) {
|
||||
clearTimer(state, slide);
|
||||
const handle = setTimeout(() => {
|
||||
state.timers.delete(slide);
|
||||
if (slide.dataset.active !== 'true') {
|
||||
slide.dataset.state = SLIDE_STATE_IDLE;
|
||||
}
|
||||
}, SLIDE_TRANSITION_TIMEOUT);
|
||||
state.timers.set(slide, handle);
|
||||
}
|
||||
|
||||
function applySlideState(state, slide, { isActive, wasActive, immediate }) {
|
||||
if (isActive) {
|
||||
clearTimer(state, slide);
|
||||
slide.dataset.active = 'true';
|
||||
slide.dataset.state = SLIDE_STATE_ACTIVE;
|
||||
slide.dataset.inert = 'false';
|
||||
slide.setAttribute('aria-hidden', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
slide.dataset.active = 'false';
|
||||
slide.dataset.inert = 'true';
|
||||
slide.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (!immediate && wasActive) {
|
||||
slide.dataset.state = SLIDE_STATE_LEAVING;
|
||||
scheduleSlideIdle(state, slide);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimer(state, slide);
|
||||
slide.dataset.state = SLIDE_STATE_IDLE;
|
||||
}
|
||||
|
||||
function updateSlides(state, { previousIndex, immediate = false } = {}) {
|
||||
const currentRange = getRange(state.index, state.perPage);
|
||||
const previousRange =
|
||||
typeof previousIndex === 'number' ? getRange(previousIndex, state.perPage) : currentRange;
|
||||
|
||||
state.slides.forEach((slide, slideIndex) => {
|
||||
const isActive = slideIndex >= currentRange.start && slideIndex < currentRange.end;
|
||||
const wasActive = slideIndex >= previousRange.start && slideIndex < previousRange.end;
|
||||
applySlideState(state, slide, { isActive, wasActive, immediate });
|
||||
});
|
||||
}
|
||||
|
||||
function syncTransitionMode(container, track, mode) {
|
||||
if (!container || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
container.dataset.transition = mode;
|
||||
track.dataset.transition = mode;
|
||||
} else {
|
||||
delete container.dataset.transition;
|
||||
delete track.dataset.transition;
|
||||
}
|
||||
}
|
||||
|
||||
export function createCarousel(container, options = {}) {
|
||||
const track = options.track || container?.querySelector('[data-carousel-track]');
|
||||
const slides = options.slides || toArray(track?.children);
|
||||
@@ -15,45 +92,63 @@ export function createCarousel(container, options = {}) {
|
||||
}
|
||||
|
||||
const perPage = Math.max(1, Math.min(options.perPage || 1, slides.length));
|
||||
const pageCount = Math.max(1, Math.ceil(slides.length / perPage));
|
||||
|
||||
const state = {
|
||||
container,
|
||||
track,
|
||||
slides,
|
||||
perPage,
|
||||
pageCount,
|
||||
loop: Boolean(options.loop),
|
||||
autoPlayInterval: Math.max(0, options.autoPlayInterval || 0),
|
||||
index: 0,
|
||||
progress: null,
|
||||
cleanup: [],
|
||||
timers: new Map(),
|
||||
setProgress(progressInstance) {
|
||||
this.progress = progressInstance || null;
|
||||
},
|
||||
};
|
||||
|
||||
container.classList.add('splide', 'is-initialized');
|
||||
container.classList.add('is-active');
|
||||
applyDynamicStyles(track, { transition: 'transform 420ms ease' }, { slot: 'transition' });
|
||||
container.dataset.count = String(pageCount);
|
||||
container.dataset.perPage = String(perPage);
|
||||
container.dataset.index = '0';
|
||||
container.dataset.state = pageCount > 1 ? 'active' : 'idle';
|
||||
|
||||
const basis = 100 / perPage;
|
||||
slides.forEach((slide) => {
|
||||
slide.classList.add('splide__slide');
|
||||
applyDynamicStyles(slide, { flex: `0 0 ${basis}%`, maxWidth: `${basis}%` }, { slot: 'size' });
|
||||
syncTransitionMode(container, track, 'auto');
|
||||
|
||||
slides.forEach((slide, slideIndex) => {
|
||||
slide.dataset.index = String(slideIndex);
|
||||
slide.dataset.active = 'false';
|
||||
slide.dataset.state = SLIDE_STATE_IDLE;
|
||||
slide.dataset.inert = 'true';
|
||||
slide.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
function updateActiveSlides() {
|
||||
const offset = (state.index * 100) / state.perPage;
|
||||
applyDynamicStyles(track, { transform: `translateX(-${offset}%)` }, { slot: 'position' });
|
||||
|
||||
state.slides.forEach((slide, idx) => {
|
||||
const inRange = idx >= state.index && idx < state.index + state.perPage;
|
||||
slide.classList.toggle('is-active', inRange);
|
||||
slide.setAttribute('aria-hidden', inRange ? 'false' : 'true');
|
||||
});
|
||||
function handlePrev(event) {
|
||||
event?.preventDefault?.();
|
||||
advanceSlide(state, -1);
|
||||
}
|
||||
|
||||
state.goTo = function goTo(targetIndex, { restartProgress = true } = {}) {
|
||||
const maxIndex = Math.max(0, state.slides.length - state.perPage);
|
||||
let nextIndex = targetIndex;
|
||||
function handleNext(event) {
|
||||
event?.preventDefault?.();
|
||||
advanceSlide(state, 1);
|
||||
}
|
||||
|
||||
if (options.controls?.prev) {
|
||||
options.controls.prev.addEventListener('click', handlePrev);
|
||||
state.cleanup.push(() => options.controls.prev.removeEventListener('click', handlePrev));
|
||||
}
|
||||
|
||||
if (options.controls?.next) {
|
||||
options.controls.next.addEventListener('click', handleNext);
|
||||
state.cleanup.push(() => options.controls.next.removeEventListener('click', handleNext));
|
||||
}
|
||||
|
||||
state.goTo = function goTo(targetPage, { restartProgress = true, instant = false } = {}) {
|
||||
const maxIndex = state.pageCount - 1;
|
||||
let nextIndex = targetPage;
|
||||
|
||||
if (nextIndex < 0) {
|
||||
nextIndex = state.loop ? maxIndex : 0;
|
||||
@@ -61,8 +156,18 @@ export function createCarousel(container, options = {}) {
|
||||
nextIndex = state.loop ? 0 : maxIndex;
|
||||
}
|
||||
|
||||
const previousIndex = state.index;
|
||||
state.index = nextIndex;
|
||||
updateActiveSlides();
|
||||
|
||||
container.dataset.index = String(nextIndex);
|
||||
|
||||
syncTransitionMode(container, track, instant ? 'instant' : 'auto');
|
||||
|
||||
updateSlides(state, { previousIndex, immediate: instant });
|
||||
|
||||
if (instant) {
|
||||
syncTransitionMode(container, track, 'auto');
|
||||
}
|
||||
|
||||
if (restartProgress && state.autoPlayInterval > 0 && state.progress) {
|
||||
state.progress.reset();
|
||||
@@ -70,26 +175,8 @@ export function createCarousel(container, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const prevHandler = (event) => {
|
||||
event?.preventDefault();
|
||||
advanceSlide(state, -1);
|
||||
};
|
||||
const nextHandler = (event) => {
|
||||
event?.preventDefault();
|
||||
advanceSlide(state, 1);
|
||||
};
|
||||
state.goTo(0, { restartProgress: false, instant: true });
|
||||
|
||||
if (options.controls?.prev) {
|
||||
options.controls.prev.addEventListener('click', prevHandler);
|
||||
state.cleanup.push(() => options.controls.prev.removeEventListener('click', prevHandler));
|
||||
}
|
||||
|
||||
if (options.controls?.next) {
|
||||
options.controls.next.addEventListener('click', nextHandler);
|
||||
state.cleanup.push(() => options.controls.next.removeEventListener('click', nextHandler));
|
||||
}
|
||||
|
||||
state.goTo(state.index, { restartProgress: false });
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -104,6 +191,7 @@ export function destroyCarousel(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.cleanup?.forEach((dispose) => {
|
||||
try {
|
||||
dispose();
|
||||
@@ -113,12 +201,31 @@ export function destroyCarousel(state) {
|
||||
});
|
||||
state.cleanup = [];
|
||||
|
||||
for (const handle of state.timers.values()) {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
state.timers.clear();
|
||||
|
||||
if (state.progress && typeof state.progress.destroy === 'function') {
|
||||
state.progress.destroy();
|
||||
}
|
||||
|
||||
if (state.container) {
|
||||
state.container.classList.remove('is-initialized');
|
||||
state.container.classList.remove('is-active');
|
||||
delete state.container.dataset.index;
|
||||
delete state.container.dataset.count;
|
||||
delete state.container.dataset.perPage;
|
||||
delete state.container.dataset.state;
|
||||
delete state.container.dataset.transition;
|
||||
}
|
||||
|
||||
if (state.track) {
|
||||
delete state.track.dataset.transition;
|
||||
}
|
||||
|
||||
state.slides?.forEach((slide) => {
|
||||
delete slide.dataset.active;
|
||||
delete slide.dataset.state;
|
||||
delete slide.dataset.inert;
|
||||
slide.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
}
|
||||
+26
-15
@@ -1,7 +1,6 @@
|
||||
import { createCarousel, advanceSlide, destroyCarousel } from './carousel.js';
|
||||
import { mountProgressBar } from './progressBar.js';
|
||||
import { initZapthreadsEmbed as defaultInitZapthreadsEmbed } from './zapthreadsEmbed.js';
|
||||
import { applyDynamicStyles } from '../ui/styleSystem.js';
|
||||
|
||||
const CAROUSEL_AUTOPLAY_INTERVAL = 8000;
|
||||
|
||||
@@ -235,8 +234,18 @@ function renderTopNotes(notes) {
|
||||
function createArrowButton(direction) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = `splide__arrow splide__arrow--${direction} flex h-10 w-10 items-center justify-center rounded-full border border-blog-neutral-200 text-xl text-blog-gray-500 transition hover:text-blog-purple dark:border-blog-neutral-700 dark:text-blog-gray-200`;
|
||||
const sideClass =
|
||||
direction === 'next'
|
||||
? 'right-0 translate-x-1/2'
|
||||
: 'left-0 -translate-x-1/2';
|
||||
button.className = `blog-carousel__control pointer-events-auto absolute top-1/2 z-10 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-blog-neutral-200 bg-white/80 text-xl text-blog-gray-500 shadow-sm transition hover:text-blog-purple dark:border-blog-neutral-700 dark:bg-blog-neutral-900/80 dark:text-blog-gray-200 ${sideClass}`;
|
||||
button.setAttribute('aria-label', direction === 'next' ? 'Next slide' : 'Previous slide');
|
||||
button.setAttribute('data-carousel-control', direction);
|
||||
if (direction === 'next') {
|
||||
button.setAttribute('data-carousel-next', 'true');
|
||||
} else {
|
||||
button.setAttribute('data-carousel-prev', 'true');
|
||||
}
|
||||
button.textContent = direction === 'next' ? '›' : '‹';
|
||||
return button;
|
||||
}
|
||||
@@ -247,28 +256,31 @@ function renderShortNotesCarousel(notes, config) {
|
||||
}
|
||||
|
||||
const section = document.createElement('section');
|
||||
section.className = 'short-notes splide flex flex-col gap-4';
|
||||
section.className = 'short-notes blog-carousel relative flex flex-col gap-4';
|
||||
section.setAttribute('data-carousel', 'true');
|
||||
|
||||
const heading = document.createElement('h2');
|
||||
heading.className = 'text-2xl font-semibold text-blog-gray-800 dark:text-blog-gray-100';
|
||||
heading.textContent = 'Short notes';
|
||||
section.appendChild(heading);
|
||||
|
||||
const trackWrapper = document.createElement('div');
|
||||
trackWrapper.className = 'splide__track overflow-hidden';
|
||||
section.appendChild(trackWrapper);
|
||||
const viewport = document.createElement('div');
|
||||
viewport.className = 'blog-carousel__viewport relative overflow-hidden';
|
||||
viewport.setAttribute('data-carousel-viewport', 'true');
|
||||
section.appendChild(viewport);
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'splide__list flex gap-4';
|
||||
list.className = 'blog-carousel__track';
|
||||
list.setAttribute('data-carousel-track', 'true');
|
||||
trackWrapper.appendChild(list);
|
||||
viewport.appendChild(list);
|
||||
|
||||
const perPage = Math.min(notes.length, 3);
|
||||
const summariesLimit = config.shortFeedSummaryMaxChars || 300;
|
||||
|
||||
notes.forEach((note) => {
|
||||
const item = document.createElement('li');
|
||||
item.className = 'splide__slide flex flex-col gap-2 rounded-lg bg-blog-neutral-50 p-4 dark:bg-blog-neutral-900';
|
||||
item.className =
|
||||
'blog-carousel__slide flex flex-col gap-2 rounded-lg bg-blog-neutral-50 p-4 transition-colors dark:bg-blog-neutral-900';
|
||||
item.setAttribute('data-carousel-slide', 'true');
|
||||
|
||||
const link = document.createElement('a');
|
||||
@@ -291,8 +303,8 @@ function renderShortNotesCarousel(notes, config) {
|
||||
|
||||
const prev = createArrowButton('prev');
|
||||
const next = createArrowButton('next');
|
||||
section.appendChild(prev);
|
||||
section.appendChild(next);
|
||||
viewport.appendChild(prev);
|
||||
viewport.appendChild(next);
|
||||
|
||||
const carousel = createCarousel(section, {
|
||||
track: list,
|
||||
@@ -381,8 +393,8 @@ function renderMainFeed(notes) {
|
||||
notes.forEach((note) => {
|
||||
const article = document.createElement('article');
|
||||
article.id = note.id;
|
||||
article.className = 'flex flex-col gap-4 border-b border-blog-neutral-200 pb-8 last:border-b-0 dark:border-blog-neutral-800';
|
||||
applyDynamicStyles(article, { scrollMarginTop: '6rem' }, { slot: 'anchor-offset' });
|
||||
article.className =
|
||||
'flex scroll-mt-24 flex-col gap-4 border-b border-blog-neutral-200 pb-8 last:border-b-0 dark:border-blog-neutral-800';
|
||||
|
||||
const header = document.createElement('header');
|
||||
header.className = 'flex flex-col gap-2';
|
||||
@@ -596,8 +608,7 @@ function mountBlogApp({ initZapthreadsEmbed = defaultInitZapthreadsEmbed } = {})
|
||||
}
|
||||
|
||||
const main = document.createElement('main');
|
||||
main.className = 'mx-auto flex w-full max-w-5xl flex-col gap-12 px-4 py-12';
|
||||
applyDynamicStyles(main, { scrollPaddingTop: '6rem' }, { slot: 'scroll-padding' });
|
||||
main.className = 'mx-auto flex w-full max-w-5xl flex-col gap-12 px-4 py-12 scroll-pt-24';
|
||||
main.appendChild(hero);
|
||||
|
||||
const topNotes = config.topNotes > 0 ? notes.slice(0, config.topNotes) : [];
|
||||
|
||||
@@ -7,15 +7,17 @@ export function mountProgressBar(
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className =
|
||||
'splide__progress mt-2 h-1 w-full overflow-hidden rounded-full bg-blog-neutral-100 dark:bg-blog-neutral-800';
|
||||
wrapper.className = 'blog-carousel__progress blog-progress mt-2';
|
||||
wrapper.dataset.state = 'idle';
|
||||
wrapper.setAttribute('data-carousel-progress', 'true');
|
||||
|
||||
const meter = document.createElement('progress');
|
||||
meter.className = 'splide__progress__bar progress progress--blog transition-none';
|
||||
meter.className = 'progress progress--blog';
|
||||
meter.max = 100;
|
||||
meter.value = 0;
|
||||
meter.dataset.progress = '0';
|
||||
meter.dataset.state = 'idle';
|
||||
meter.setAttribute('data-progress-meter', 'true');
|
||||
meter.setAttribute('data-variant', 'blog');
|
||||
meter.setAttribute('aria-label', 'Carousel progress');
|
||||
meter.setAttribute('aria-valuetext', 'Carousel progress 0% complete');
|
||||
@@ -35,6 +37,7 @@ export function mountProgressBar(
|
||||
}
|
||||
|
||||
function setState(state) {
|
||||
wrapper.dataset.state = state;
|
||||
meter.dataset.state = state;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,17 @@ function assertNoInlineStyleAttributes(elements, messagePrefix = 'element') {
|
||||
}
|
||||
|
||||
const CAROUSEL_HTML = `
|
||||
<div data-carousel>
|
||||
<div data-carousel-track>
|
||||
<article data-slide="0">Slide 1</article>
|
||||
<article data-slide="1">Slide 2</article>
|
||||
<article data-slide="2">Slide 3</article>
|
||||
<section class="blog-carousel" data-carousel>
|
||||
<div data-carousel-viewport>
|
||||
<ul data-carousel-track>
|
||||
<li data-carousel-slide>Slide 1</li>
|
||||
<li data-carousel-slide>Slide 2</li>
|
||||
<li data-carousel-slide>Slide 3</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" data-carousel-prev>Prev</button>
|
||||
<button type="button" data-carousel-next>Next</button>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
const PROGRESS_HTML = '<div id="progress-root"></div>';
|
||||
@@ -62,7 +64,7 @@ describe('blog carousel', () => {
|
||||
env.cleanup();
|
||||
});
|
||||
|
||||
test('initializes slides with dynamic styling and accessibility state', () => {
|
||||
test('initializes slides with data attributes and accessibility state', () => {
|
||||
const state = createCarousel(container, {
|
||||
perPage: 1,
|
||||
loop: true,
|
||||
@@ -72,27 +74,28 @@ describe('blog carousel', () => {
|
||||
assert.ok(state, 'state should be created when container markup is valid');
|
||||
assert.strictEqual(state.perPage, 1);
|
||||
assert.strictEqual(state.slides.length, 3);
|
||||
assert.ok(container.classList.contains('splide'));
|
||||
assert.ok(container.classList.contains('is-initialized'));
|
||||
assert.ok(container.classList.contains('is-active'));
|
||||
assert.strictEqual(state.pageCount, 3);
|
||||
|
||||
const trackDynamicClasses = Array.from(track.classList).filter((className) => className.startsWith('bvds-'));
|
||||
assert.ok(trackDynamicClasses.length >= 1, 'track should receive dynamic style classes');
|
||||
assert.strictEqual(container.dataset.index, '0');
|
||||
assert.strictEqual(container.dataset.perPage, '1');
|
||||
assert.strictEqual(container.dataset.count, '3');
|
||||
assert.strictEqual(container.dataset.state, 'active');
|
||||
assert.strictEqual(track.dataset.transition, 'auto');
|
||||
|
||||
assertNoInlineStyleAttributes([container, track, ...slides], 'carousel element');
|
||||
|
||||
for (const slide of slides) {
|
||||
assert.ok(slide.classList.contains('splide__slide'));
|
||||
const dynamicClasses = Array.from(slide.classList).filter((className) => className.startsWith('bvds-'));
|
||||
assert.ok(dynamicClasses.length >= 1, 'slide should receive dynamic sizing styles');
|
||||
}
|
||||
|
||||
assert.strictEqual(slides[0].getAttribute('aria-hidden'), 'false');
|
||||
assert.strictEqual(slides[1].getAttribute('aria-hidden'), 'true');
|
||||
assert.strictEqual(slides[2].getAttribute('aria-hidden'), 'true');
|
||||
slides.forEach((slide, index) => {
|
||||
assert.strictEqual(slide.dataset.index, String(index));
|
||||
assert.strictEqual(slide.dataset.active, index === 0 ? 'true' : 'false');
|
||||
assert.strictEqual(slide.dataset.inert, index === 0 ? 'false' : 'true');
|
||||
assert.strictEqual(slide.getAttribute('aria-hidden'), index === 0 ? 'false' : 'true');
|
||||
});
|
||||
|
||||
state.goTo(2);
|
||||
assert.strictEqual(state.index, 2);
|
||||
assert.strictEqual(container.dataset.index, '2');
|
||||
assert.strictEqual(slides[2].dataset.active, 'true');
|
||||
assert.strictEqual(slides[0].dataset.active, 'false');
|
||||
assert.strictEqual(slides[2].getAttribute('aria-hidden'), 'false');
|
||||
assert.strictEqual(slides[0].getAttribute('aria-hidden'), 'true');
|
||||
|
||||
@@ -169,6 +172,7 @@ describe('blog carousel', () => {
|
||||
const previousIndex = state.index;
|
||||
nextButton.dispatchEvent(new env.window.MouseEvent('click', { bubbles: true }));
|
||||
assert.strictEqual(state.index, previousIndex, 'clicks after destroy should not change the index');
|
||||
assert.strictEqual(container.dataset.index, undefined);
|
||||
});
|
||||
|
||||
test('advanceSlide delegates to goTo with relative offsets', () => {
|
||||
@@ -202,13 +206,14 @@ describe('progress bar', () => {
|
||||
const progress = mountProgressBar(container, { initialDuration: 500, autoStart: false });
|
||||
assert.ok(progress, 'progress instance should be returned');
|
||||
|
||||
const wrapper = container.querySelector('.splide__progress');
|
||||
const wrapper = container.querySelector('.blog-carousel__progress');
|
||||
assert.ok(wrapper, 'wrapper element should be appended');
|
||||
const meter = wrapper.querySelector('progress.splide__progress__bar');
|
||||
const meter = wrapper.querySelector('progress.progress--blog');
|
||||
assert.ok(meter, 'progress meter should be created');
|
||||
assert.strictEqual(meter.max, 100, 'progress meter should expose a 0-100 range');
|
||||
assert.strictEqual(meter.value, 0, 'progress meter should start at zero');
|
||||
assert.strictEqual(meter.dataset.state, 'idle');
|
||||
assert.strictEqual(wrapper.dataset.state, 'idle');
|
||||
assert.strictEqual(meter.dataset.progress, '0');
|
||||
assert.strictEqual(
|
||||
meter.getAttribute('aria-valuetext'),
|
||||
@@ -228,7 +233,7 @@ describe('progress bar', () => {
|
||||
const onComplete = createSpy();
|
||||
const progress = mountProgressBar(container, { initialDuration: 600, autoStart: true, onComplete });
|
||||
const wrapper = progress.element;
|
||||
const meter = wrapper.querySelector('progress.splide__progress__bar');
|
||||
const meter = wrapper.querySelector('progress.progress--blog');
|
||||
assert.ok(meter, 'progress meter should exist when autoStart is enabled');
|
||||
|
||||
assertNoInlineStyleAttributes([container, wrapper, meter], 'progress element');
|
||||
@@ -252,7 +257,7 @@ describe('progress bar', () => {
|
||||
test('manual controls support stop, restart, and cleanup', () => {
|
||||
const onComplete = createSpy();
|
||||
const progress = mountProgressBar(container, { initialDuration: 800, onComplete });
|
||||
const meter = progress.element.querySelector('progress.splide__progress__bar');
|
||||
const meter = progress.element.querySelector('progress.progress--blog');
|
||||
|
||||
progress.start(800);
|
||||
env.raf.step(0);
|
||||
|
||||
@@ -78,7 +78,7 @@ export function installInlineStyleGuard(document) {
|
||||
prototype.setAttribute = function inlineStyleSafeSetAttribute(name, value) {
|
||||
if (typeof name === 'string' && name.toLowerCase() === 'style') {
|
||||
throw new Error(
|
||||
'Inline style attributes are disabled in blog widget tests. Use applyDynamicStyles or CSS class toggles instead.',
|
||||
'Inline style attributes are disabled in blog widget tests. Use CSS classes or data attributes instead.',
|
||||
);
|
||||
}
|
||||
return original.call(this, name, value);
|
||||
@@ -93,7 +93,7 @@ export function installInlineStyleGuard(document) {
|
||||
prototype.setAttributeNS = function inlineStyleSafeSetAttributeNS(namespace, name, value) {
|
||||
if (typeof name === 'string' && name.toLowerCase() === 'style') {
|
||||
throw new Error(
|
||||
'Inline style attributes are disabled in blog widget tests. Use applyDynamicStyles or CSS class toggles instead.',
|
||||
'Inline style attributes are disabled in blog widget tests. Use CSS classes or data attributes instead.',
|
||||
);
|
||||
}
|
||||
return original.call(this, namespace, name, value);
|
||||
@@ -116,12 +116,12 @@ export function installInlineStyleGuard(document) {
|
||||
Object.defineProperty(element, 'style', {
|
||||
get() {
|
||||
throw new Error(
|
||||
'Inline style access is disabled in blog widget tests. Use applyDynamicStyles or CSS class toggles instead.',
|
||||
'Inline style access is disabled in blog widget tests. Use CSS classes or data attributes instead.',
|
||||
);
|
||||
},
|
||||
set() {
|
||||
throw new Error(
|
||||
'Inline style assignment is disabled in blog widget tests. Use applyDynamicStyles or CSS class toggles instead.',
|
||||
'Inline style assignment is disabled in blog widget tests. Use CSS classes or data attributes instead.',
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
|
||||
Reference in new issue
Block a user