diff --git a/Components/GridSuperPost/_style.scss b/Components/GridSuperPost/_style.scss new file mode 100644 index 00000000..e4869570 --- /dev/null +++ b/Components/GridSuperPost/_style.scss @@ -0,0 +1,238 @@ +flynt-component[name='GridSuperPost'] { + --grid-super-post-transition: 0.4s cubic-bezier(0.16, 1, 0.3, 1); + + .grid { + --grid-item-width: calc((var(--content-max-width-full) - 2 * var(--flow-space)) / 3); + display: grid; + gap: var(--flow-space); + grid-template-columns: repeat(auto-fit, min(var(--grid-item-width), 100%)); + justify-content: center; + list-style: none; + padding: 0; + } + + .card { + isolation: isolate; + overflow: hidden; + position: relative; + transition: var(--grid-super-post-transition); + + .link { + block-size: 100%; + color: currentColor; + display: flex; + flex-direction: column; + font-weight: initial; + text-decoration: none; + } + + .figure { + margin: 0; + overflow: hidden; + position: relative; + } + + .card-image { + display: block; + inline-size: 100%; + transition: transform 0.6s ease; + } + + .card-glow { + display: none; + } + + .content { + color: var(--color-text); + flex-grow: 1; + margin-block-start: 0; + } + + .category { + color: var(--color-accent); + display: block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + margin-block-end: 8px; + text-transform: uppercase; + } + + .title { + margin-block-start: 0; + } + + .excerpt { + color: var(--color-text-muted); + } + + .footer { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + gap: 4px 12px; + inline-size: 100%; + padding-block: 0 var(--box-spacing); + padding-inline: var(--box-spacing); + position: relative; + } + } + + // Hover Style: Lift + Shadow + .card[data-hover='liftShadow'] { + &:hover, + &:focus-within { + box-shadow: 0 24px 48px -20px rgba(0, 0, 0, 0.35); + transform: translateY(-8px); + } + } + + // Hover Style: Zoom + Overlay + .card[data-hover='zoomOverlay'] { + .figure::after { + background: linear-gradient(to top, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0) 60%); + content: ''; + inset: 0; + opacity: 0; + position: absolute; + transition: opacity 0.4s ease; + } + + &:hover, + &:focus-within { + .card-image { + transform: scale(1.08); + } + + .figure::after { + opacity: 1; + } + } + } + + // Hover Style: Gradient Border Glow + .card[data-hover='gradientBorder'] { + border-radius: 18px; + overflow: visible; + + .figure { + border-radius: 16px; + overflow: hidden; + } + + &::before { + animation: gridSuperPostRotateGradient 4s linear infinite paused; + background: conic-gradient(from var(--grid-super-post-angle, 0deg), #ff5f6d, #7f00ff, #00c6ff, #38ef7d, #ffd200, #ff5f6d); + border-radius: inherit; + content: ''; + inset: -2px; + opacity: 0; + position: absolute; + transition: opacity 0.4s ease; + z-index: -1; + } + + &:hover, + &:focus-within { + &::before { + animation-play-state: running; + opacity: 1; + } + } + } + + // Hover Style: Tilt 3D + .card[data-hover='tilt3d'] { + transform: perspective(900px) rotateX(var(--grid-super-post-ry, 0deg)) rotateY(var(--grid-super-post-rx, 0deg)) scale(var(--grid-super-post-scale, 1)); + transform-style: preserve-3d; + + &:hover, + &:focus-within { + --grid-super-post-scale: 1.03; + + box-shadow: 0 30px 60px -24px rgba(0, 0, 0, 0.4); + } + } + + // Hover Style: Holo Card + .card[data-hover='holo'] { + transform: perspective(900px) rotateX(var(--grid-super-post-ry, 0deg)) rotateY(var(--grid-super-post-rx, 0deg)); + transform-style: preserve-3d; + + .card-glow { + background: linear-gradient(115deg, transparent 20%, rgba(255, 0, 231, 0.65) 36%, rgba(0, 224, 255, 0.65) 44%, rgba(255, 214, 0, 0.65) 52%, transparent 68%); + background-position: var(--grid-super-post-mx, 50%) var(--grid-super-post-my, 50%); + background-size: 220% 220%; + display: block; + inset: 0; + mix-blend-mode: color-dodge; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 0.3s ease; + } + + &:hover, + &:focus-within { + .card-glow { + opacity: 0.85; + } + } + } + + // Hover Style: Glassmorphism + Shimmer Reveal + .card[data-hover='glassmorphism'] { + .card-glow { + background: rgba(255, 255, 255, 0.08); + display: block; + inset: 0; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 0.4s ease, backdrop-filter 0.4s ease; + + &::after { + background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, 0.55) 50%, transparent 60%); + background-position: var(--grid-super-post-mx, -20%) var(--grid-super-post-my, -20%); + background-size: 220% 220%; + content: ''; + inset: 0; + position: absolute; + } + } + + &:hover, + &:focus-within { + .card-glow { + backdrop-filter: blur(3px); + opacity: 1; + } + } + } + + @media (prefers-reduced-motion: reduce) { + .card, + .card-image, + .card-glow { + transition: none; + } + + .card[data-hover='gradientBorder']::before { + animation: none; + opacity: 0.6; + } + } +} + +@keyframes gridSuperPostRotateGradient { + to { + --grid-super-post-angle: 360deg; + } +} + +@property --grid-super-post-angle { + inherits: false; + initial-value: 0deg; + syntax: ''; +} diff --git a/Components/GridSuperPost/functions.php b/Components/GridSuperPost/functions.php new file mode 100644 index 00000000..e754670e --- /dev/null +++ b/Components/GridSuperPost/functions.php @@ -0,0 +1,386 @@ + getCardsFromPostType($data['postType'] ?? 'post', $maxPosts, $excerptWords), + 'category' => getCardsFromCategory($data['taxonomies'] ?? [], $maxPosts, $excerptWords), + default => getCardsFromManual($data['manualCards'] ?? [], $excerptWords), + }; + + return $data; +}); + +function getCardsFromPostType(string $postType, int $maxPosts, int $excerptWords): array +{ + $posts = Timber::get_posts([ + 'post_status' => 'publish', + 'post_type' => $postType, + 'posts_per_page' => $maxPosts, + 'ignore_sticky_posts' => 1, + ]); + + return array_map(fn ($post) => buildCardFromPost($post, $excerptWords), $posts->to_array()); +} + +function getCardsFromCategory(array $taxonomies, int $maxPosts, int $excerptWords): array +{ + $posts = Timber::get_posts([ + 'post_status' => 'publish', + 'post_type' => 'post', + 'cat' => implode(',', array_map(fn ($term) => $term->term_id, $taxonomies)), + 'posts_per_page' => $maxPosts, + 'ignore_sticky_posts' => 1, + ]); + + return array_map(fn ($post) => buildCardFromPost($post, $excerptWords), $posts->to_array()); +} + +function buildCardFromPost($post, int $excerptWords): array +{ + $terms = $post->terms('category'); + $author = $post->author(); + + return [ + 'title' => $post->title(), + 'link' => $post->link(), + 'image' => normalizeImage($post->thumbnail(), $post->title()), + 'category' => $terms ? $terms[0]->name : null, + 'date' => $post->date('M j, Y'), + 'content' => $post->content(), + 'author' => $author ? $author->name() : null, + 'excerpt' => $excerptWords > 0 ? (string) $post->excerpt()->length($excerptWords)->read_more(false) : '', + 'hoverStyle' => null, + ]; +} + +function getCardsFromManual(array $manualCards, int $excerptWords): array +{ + return array_map(function ($card) use ($excerptWords) { + $excerpt = $card['excerpt'] ?? ''; + + return [ + 'title' => $card['title'] ?? '', + 'link' => $card['link'] ?? '', + 'image' => normalizeImage($card['featuredImage'] ?? null, $card['title'] ?? ''), + 'category' => $card['category'] ?: null, + 'date' => $card['date'] ?: null, + 'content' => null, + 'author' => $card['author'] ?: null, + 'excerpt' => $excerptWords > 0 && $excerpt ? wp_trim_words($excerpt, $excerptWords) : '', + 'hoverStyle' => $card['hoverStyle'] ?: null, + ]; + }, $manualCards); +} + +function normalizeImage($image, string $fallbackAlt): ?array +{ + if (!$image) { + return null; + } + + return [ + 'src' => $image->src(), + 'alt' => $image->alt() ?: $fallbackAlt, + ]; +} + +function getHoverStyleChoices(): array +{ + return [ + 'none' => __('None', 'flynt'), + 'liftShadow' => __('Lift + Shadow', 'flynt'), + 'zoomOverlay' => __('Zoom + Overlay', 'flynt'), + 'gradientBorder' => __('Gradient Border Glow', 'flynt'), + 'tilt3d' => __('Tilt 3D', 'flynt'), + 'holo' => __('Holo Card', 'flynt'), + 'glassmorphism' => __('Glassmorphism + Shimmer Reveal', 'flynt'), + ]; +} + +function getAvailablePostTypes(): array +{ + // Built with an explicit list rather than get_post_types(), since this + // runs on after_setup_theme, before plugin-registered post types (e.g. + // WooCommerce's "product", registered on init) exist yet. + $postTypes = [ + 'post' => __('Posts', 'flynt'), + 'page' => __('Pages', 'flynt'), + ]; + + if (class_exists('WooCommerce')) { + $postTypes['product'] = __('Products', 'flynt'); + } + + return $postTypes; +} + +function getACFLayout(): array +{ + $sourceIsField = fn (string $value) => [ + 'conditional_logic' => [ + [ + [ + 'fieldPath' => 'contentSource', + 'operator' => '==', + 'value' => $value, + ], + ], + ], + ]; + + return [ + 'name' => 'gridSuperPost', + 'label' => __('Grid: Super Post', 'flynt'), + 'sub_fields' => [ + [ + 'label' => __('Content', 'flynt'), + 'name' => 'contentTab', + 'type' => 'tab', + 'placement' => 'top', + 'endpoint' => 0, + ], + [ + 'label' => __('Content Source', 'flynt'), + 'name' => 'contentSource', + 'type' => 'button_group', + 'choices' => [ + 'manual' => __('Manual', 'flynt'), + 'postType' => __('Posts (CPTs)', 'flynt'), + 'category' => __('Category', 'flynt'), + ], + 'default_value' => 'manual', + ], + array_merge([ + 'label' => __('Cards', 'flynt'), + 'name' => 'manualCards', + 'type' => 'repeater', + 'min' => 1, + 'layout' => 'block', + 'button_label' => __('Add Card', 'flynt'), + 'sub_fields' => [ + [ + 'label' => __('Title', 'flynt'), + 'name' => 'title', + 'type' => 'text', + 'required' => 1, + ], + [ + 'label' => __('Featured Image', 'flynt'), + 'name' => 'featuredImage', + 'type' => 'image', + 'preview_size' => 'medium', + 'mime_types' => 'jpg,jpeg,png,webp', + ], + [ + 'label' => __('Excerpt', 'flynt'), + 'name' => 'excerpt', + 'type' => 'textarea', + 'rows' => 3, + ], + [ + 'label' => __('Link', 'flynt'), + 'name' => 'link', + 'type' => 'url', + ], + [ + 'label' => __('Category', 'flynt'), + 'name' => 'category', + 'type' => 'text', + 'wrapper' => ['width' => 34], + ], + [ + 'label' => __('Date', 'flynt'), + 'name' => 'date', + 'type' => 'text', + 'wrapper' => ['width' => 33], + ], + [ + 'label' => __('Author', 'flynt'), + 'name' => 'author', + 'type' => 'text', + 'wrapper' => ['width' => 33], + ], + [ + 'label' => __('Hover Style', 'flynt'), + 'instructions' => __('Overrides the grid-wide Hover Style option for this card only.', 'flynt'), + 'name' => 'hoverStyle', + 'type' => 'select', + 'choices' => array_merge(['' => __('Use Default (Grid)', 'flynt')], getHoverStyleChoices()), + 'default_value' => '', + 'allow_null' => 0, + 'multiple' => 0, + 'ui' => 0, + ], + ], + ], $sourceIsField('manual')), + array_merge([ + 'label' => __('Post Type', 'flynt'), + 'name' => 'postType', + 'type' => 'select', + 'choices' => getAvailablePostTypes(), + 'default_value' => 'post', + 'allow_null' => 0, + 'multiple' => 0, + 'ui' => 0, + ], $sourceIsField('postType')), + array_merge([ + 'label' => __('Categories', 'flynt'), + 'instructions' => __('Select 1 or more categories or leave empty to show from all posts.', 'flynt'), + 'name' => 'taxonomies', + 'type' => 'taxonomy', + 'taxonomy' => 'category', + 'field_type' => 'multi_select', + 'allow_null' => 1, + 'multiple' => 1, + 'add_term' => 0, + 'save_terms' => 0, + 'load_terms' => 0, + 'return_format' => 'object', + ], $sourceIsField('category')), + array_merge([ + 'label' => __('Max Posts', 'flynt'), + 'name' => 'maxPosts', + 'type' => 'number', + 'default_value' => DEFAULT_MAX_POSTS, + 'min' => 1, + 'step' => 1, + ], [ + 'conditional_logic' => [ + [ + [ + 'fieldPath' => 'contentSource', + 'operator' => '!=', + 'value' => 'manual', + ], + ], + ], + ]), + [ + 'label' => __('Options', 'flynt'), + 'name' => 'optionsTab', + 'type' => 'tab', + 'placement' => 'top', + 'endpoint' => 0, + ], + [ + 'label' => '', + 'name' => 'options', + 'type' => 'group', + 'layout' => 'row', + 'sub_fields' => [ + FieldVariables\getTheme(), + [ + 'label' => __('Hover Style', 'flynt'), + 'instructions' => __('Default hover style for all cards. Manual cards can override this individually.', 'flynt'), + 'name' => 'hoverStyle', + 'type' => 'select', + 'choices' => getHoverStyleChoices(), + 'default_value' => 'liftShadow', + 'allow_null' => 0, + 'multiple' => 0, + 'ui' => 0, + ], + [ + 'label' => __('Show Featured Image', 'flynt'), + 'name' => 'showFeaturedImage', + 'type' => 'true_false', + 'default_value' => 1, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Show Category', 'flynt'), + 'name' => 'showCategory', + 'type' => 'true_false', + 'default_value' => 1, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Show Title', 'flynt'), + 'name' => 'showTitle', + 'type' => 'true_false', + 'default_value' => 1, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Show Date', 'flynt'), + 'name' => 'showDate', + 'type' => 'true_false', + 'default_value' => 0, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Show Reading Time', 'flynt'), + 'instructions' => __('Ignored for manually added cards.', 'flynt'), + 'name' => 'showReadingTime', + 'type' => 'true_false', + 'default_value' => 0, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Show Author', 'flynt'), + 'name' => 'showAuthor', + 'type' => 'true_false', + 'default_value' => 0, + 'ui' => 1, + 'wrapper' => ['width' => 25], + ], + [ + 'label' => __('Excerpt Words', 'flynt'), + 'instructions' => __('0 hides the excerpt.', 'flynt'), + 'name' => 'excerptWords', + 'type' => 'number', + 'default_value' => 0, + 'min' => 0, + 'step' => 1, + 'wrapper' => ['width' => 25], + ], + ], + ], + ], + ]; +} + +Options::addTranslatable('GridSuperPost', [ + [ + 'label' => __('Labels', 'flynt'), + 'name' => 'labelsTab', + 'type' => 'tab', + 'placement' => 'top', + 'endpoint' => 0, + ], + [ + 'label' => '', + 'name' => 'labels', + 'type' => 'group', + 'sub_fields' => [ + [ + 'label' => __('Reading Time - (20) min read', 'flynt'), + 'instructions' => __('%d is placeholder for number of minutes', 'flynt'), + 'name' => 'readingTime', + 'type' => 'text', + 'default_value' => __('%d min read', 'flynt'), + 'required' => 1, + ], + ], + ], +]); diff --git a/Components/GridSuperPost/index.twig b/Components/GridSuperPost/index.twig new file mode 100644 index 00000000..a1e947f2 --- /dev/null +++ b/Components/GridSuperPost/index.twig @@ -0,0 +1,61 @@ +{% if cards|length > 0 %} + + + +{% endif %} diff --git a/Components/GridSuperPost/script.js b/Components/GridSuperPost/script.js new file mode 100644 index 00000000..d22bd17d --- /dev/null +++ b/Components/GridSuperPost/script.js @@ -0,0 +1,44 @@ +const POINTER_TRACKING_STYLES = ['tilt3d', 'holo', 'glassmorphism'] + +export default function (el) { + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + if (reducedMotion) return + + // Each card can have its own hover style (falling back to the grid-wide + // default), so only the cards that actually need pointer tracking get it. + const cards = Array.from(el.querySelectorAll('[data-ref="card"]')) + .filter(card => POINTER_TRACKING_STYLES.includes(card.dataset.hover)) + if (cards.length === 0) return + + const onPointerMove = (e) => { + const card = e.currentTarget + const rect = card.getBoundingClientRect() + const x = (e.clientX - rect.left) / rect.width + const y = (e.clientY - rect.top) / rect.height + + card.style.setProperty('--grid-super-post-mx', `${x * 100}%`) + card.style.setProperty('--grid-super-post-my', `${y * 100}%`) + card.style.setProperty('--grid-super-post-rx', `${(x - 0.5) * 18}deg`) + card.style.setProperty('--grid-super-post-ry', `${(0.5 - y) * 18}deg`) + } + + const onPointerLeave = (e) => { + const card = e.currentTarget + card.style.removeProperty('--grid-super-post-mx') + card.style.removeProperty('--grid-super-post-my') + card.style.removeProperty('--grid-super-post-rx') + card.style.removeProperty('--grid-super-post-ry') + } + + cards.forEach(card => { + card.addEventListener('pointermove', onPointerMove) + card.addEventListener('pointerleave', onPointerLeave) + }) + + return () => { + cards.forEach(card => { + card.removeEventListener('pointermove', onPointerMove) + card.removeEventListener('pointerleave', onPointerLeave) + }) + } +} diff --git a/Components/ShowcaseExpandingCards/_style.scss b/Components/ShowcaseExpandingCards/_style.scss new file mode 100644 index 00000000..bd3c5358 --- /dev/null +++ b/Components/ShowcaseExpandingCards/_style.scss @@ -0,0 +1,311 @@ +flynt-component[name='ShowcaseExpandingCards'] { + --expanding-cards-duration: 700ms; + --expanding-cards-easing: cubic-bezier(0.22, 1, 0.36, 1); + --expanding-cards-transition: var(--expanding-cards-duration) var(--expanding-cards-easing); + + .expanding-cards-header { + margin-block-end: 48px; + } + + .expanding-cards-eyebrow { + background: var(--color-background-secondary); + border-radius: 999px; + color: var(--color-accent); + display: inline-block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.06em; + margin-block-end: 16px; + padding: 6px 14px; + text-transform: uppercase; + } + + .expanding-cards-title { + color: var(--color-text); + font-size: clamp(28px, 4vw, 44px); + font-weight: 900; + letter-spacing: -0.01em; + margin: 0; + } + + // Per-card accent gradients, cycled by position so the showcase reads as + // colorful/varied rather than one repeated brand color. + $expanding-cards-accents: ( + linear-gradient(135deg, #ff5f6d, #ffc371), + linear-gradient(135deg, #7f00ff, #e100ff), + linear-gradient(135deg, #00c6ff, #0072ff), + linear-gradient(135deg, #11998e, #38ef7d), + linear-gradient(135deg, #f7971e, #ffd200), + linear-gradient(135deg, #fc466b, #3f5efb) + ); + + .expanding-cards { + align-items: stretch; + block-size: clamp(320px, 42vw, 460px); + border-radius: 24px; + box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.4); + display: flex; + flex-direction: row; + overflow: hidden; + } + + .expanding-cards-option { + background-color: var(--color-border); + background-image: var(--optionBackground); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + filter: saturate(1) brightness(1); + flex-grow: 1; + inline-size: 0; + margin-inline: 6px; + min-inline-size: 56px; + overflow: hidden; + position: relative; + transition: var(--expanding-cards-transition), filter 0.4s ease; + + &::before { + background: #64748b; + content: ''; + inset: 0; + mix-blend-mode: color; + opacity: 0.6; + position: absolute; + transition: var(--expanding-cards-transition), opacity 0.4s ease; + } + + // Shimmer Reveal: a soft light sweep that travels across the card once + // it becomes active, layered on top of the color tint. + &::after { + background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.55) 48%, transparent 66%); + background-position: 150% 150%; + background-size: 250% 250%; + content: ''; + inset: 0; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 0.3s ease; + } + + @for $i from 1 through length($expanding-cards-accents) { + &:nth-child(#{length($expanding-cards-accents)}n + #{$i})::before { + background: nth($expanding-cards-accents, $i); + } + } + + &:first-child { + margin-inline-start: 0; + } + + &:last-child { + margin-inline-end: 0; + } + + &:hover { + filter: saturate(1.15) brightness(1.05); + } + + &.active { + border-radius: 24px; + flex-grow: 10000; + margin-inline: 6px; + + &::before { + opacity: 0.3; + } + + &::after { + animation: expandingCardsShimmer 1.1s ease forwards; + opacity: 1; + } + + .expanding-cards-shadow { + box-shadow: inset 0 -140px 120px -120px rgba(0, 0, 0, 0.85), inset 0 -140px 140px -100px rgba(0, 0, 0, 0.5); + } + + .expanding-cards-label { + align-items: center; + flex-direction: row; + inset-block: auto 20px; + inset-inline: 20px; + } + + // Holo Card: only visible while hovering a card that's already + // expanded, tracking pointer position via script.js. + &:hover .expanding-cards-holo, + &:focus-within .expanding-cards-holo { + opacity: 0.85; + } + + .expanding-cards-info { + overflow: visible; + white-space: normal; + writing-mode: horizontal-tb; + } + + .expanding-cards-main { + white-space: nowrap; + } + + .expanding-cards-sub { + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + max-inline-size: 46ch; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + } + + .expanding-cards-info > * { + inset-inline-start: 0; + opacity: 1; + } + } + + &:not(.active) { + border-radius: 18px; + + .expanding-cards-shadow { + box-shadow: inset 0 -100px 100px -80px rgba(0, 0, 0, 0.55); + } + + .expanding-cards-label { + align-items: center; + flex-direction: column; + inset-block: 16px; + inset-inline: 0; + justify-content: flex-end; + } + + .expanding-cards-info { + margin-inline-start: 0; + writing-mode: vertical-rl; + } + + .expanding-cards-main { + transform: rotate(180deg); + } + + .expanding-cards-sub { + display: none; + } + + .expanding-cards-info > * { + inset-inline-start: 0; + opacity: 1; + } + } + + @media (max-width: 700px) { + &:nth-child(n + 5) { + display: none; + } + } + + @media (max-width: 500px) { + &:nth-child(n + 4) { + display: none; + } + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + + &::before { + transition: none; + } + + &.active::after { + animation: none; + opacity: 0; + } + + .expanding-cards-holo { + opacity: 0; + transition: none; + } + } + } + + @keyframes expandingCardsShimmer { + from { + background-position: 150% 150%; + } + + to { + background-position: -50% -50%; + } + } + + .expanding-cards-shadow { + block-size: 140px; + inset-block-end: 0; + inset-inline: 0; + position: absolute; + transition: var(--expanding-cards-transition); + } + + .expanding-cards-holo { + background: linear-gradient(115deg, transparent 20%, rgba(255, 0, 231, 0.55) 36%, rgba(0, 224, 255, 0.55) 44%, rgba(255, 214, 0, 0.55) 52%, transparent 68%); + background-position: var(--expanding-cards-mx, 50%) var(--expanding-cards-my, 50%); + background-size: 220% 220%; + inset: 0; + mix-blend-mode: color-dodge; + opacity: 0; + pointer-events: none; + position: absolute; + transition: opacity 0.3s ease; + } + + .expanding-cards-label { + block-size: auto; + display: flex; + position: absolute; + transition: var(--expanding-cards-transition); + } + + .expanding-cards-info { + color: #fff; + display: flex; + flex-direction: column; + justify-content: center; + min-inline-size: 0; + overflow: hidden; + transition: var(--expanding-cards-transition); + white-space: nowrap; + + > * { + position: relative; + transition: var(--expanding-cards-transition), opacity 0.5s ease-out; + } + } + + .expanding-cards-main { + font-size: 1.15rem; + font-weight: 700; + max-inline-size: 100%; + overflow: hidden; + text-overflow: ellipsis; + transition: var(--expanding-cards-transition); + + a { + color: inherit; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + .expanding-cards-sub { + font-size: 0.875rem; + opacity: 0.85; + overflow: hidden; + text-overflow: ellipsis; + transition-delay: 0.1s; + } +} diff --git a/Components/ShowcaseExpandingCards/functions.php b/Components/ShowcaseExpandingCards/functions.php new file mode 100644 index 00000000..370e2a4a --- /dev/null +++ b/Components/ShowcaseExpandingCards/functions.php @@ -0,0 +1,218 @@ + ['src' => 'https://picsum.photos/id/1018/900/1200', 'alt' => __('Wandering', 'flynt')], + 'title' => __('Wandering', 'flynt'), + 'subtitle' => __('A short line describing this card, shown once it is expanded open.', 'flynt'), + ], + [ + 'image' => ['src' => 'https://picsum.photos/id/1015/900/1200', 'alt' => __('Stillness', 'flynt')], + 'title' => __('Stillness', 'flynt'), + 'subtitle' => __('A short line describing this card, shown once it is expanded open.', 'flynt'), + ], + [ + 'image' => ['src' => 'https://picsum.photos/id/1016/900/1200', 'alt' => __('Growth', 'flynt')], + 'title' => __('Growth', 'flynt'), + 'subtitle' => __('A short line describing this card, shown once it is expanded open.', 'flynt'), + ], + [ + 'image' => ['src' => 'https://picsum.photos/id/1019/900/1200', 'alt' => __('Clarity', 'flynt')], + 'title' => __('Clarity', 'flynt'), + 'subtitle' => __('A short line describing this card, shown once it is expanded open.', 'flynt'), + ], + ]; + } + return $data; +}); + +function getCardsFromItems($items): array +{ + $itemArray = is_array($items) ? $items : $items->to_array(); + + return array_map(fn ($post) => buildCard($post), $itemArray); +} + +function buildCard($post): array +{ + $thumbnail = $post->thumbnail(); + + return [ + 'image' => $thumbnail ? [ + 'src' => $thumbnail->src(), + 'alt' => $thumbnail->alt() ?: $post->title(), + ] : getPlaceholderImage($post->title()), + 'title' => $post->title(), + 'subtitle' => (string) $post->excerpt()->length(EXCERPT_WORDS)->read_more(false), + 'link' => $post->link(), + ]; +} + +function getPlaceholderImage(string $alt): array +{ + $svg = '' + . '' + . '' + . '' + . '' + . '' + . ''; + + return [ + 'src' => 'data:image/svg+xml;base64,' . base64_encode($svg), + 'alt' => $alt, + ]; +} + +function getACFLayout(): array +{ + // Built with an explicit list rather than get_post_types(), since this + // runs on after_setup_theme, before plugin-registered post types (e.g. + // WooCommerce's "product", registered on init) exist yet. + $itemPostTypes = ['post', 'page']; + if (class_exists('WooCommerce')) { + $itemPostTypes[] = 'product'; + } + + $sourceIsField = fn (string $value) => [ + 'conditional_logic' => [ + [ + [ + 'fieldPath' => 'source', + 'operator' => '==', + 'value' => $value + ] + ] + ], + ]; + + return [ + 'name' => 'showcaseExpandingCards', + 'label' => __('Showcase: Expanding Cards', 'flynt'), + 'sub_fields' => [ + [ + 'label' => __('Content', 'flynt'), + 'name' => 'contentTab', + 'type' => 'tab', + 'placement' => 'top', + 'endpoint' => 0 + ], + [ + 'label' => __('Subtitle', 'flynt'), + 'name' => 'subtitle', + 'type' => 'text', + ], + [ + 'label' => __('Title', 'flynt'), + 'name' => 'title', + 'type' => 'text', + ], + [ + 'label' => __('Source', 'flynt'), + 'name' => 'source', + 'type' => 'button_group', + 'choices' => [ + 'manual' => __('Manual', 'flynt'), + 'items' => __('Selected Items', 'flynt'), + ], + 'default_value' => 'manual', + ], + array_merge([ + 'label' => __('Cards', 'flynt'), + 'name' => 'cards', + 'type' => 'repeater', + 'min' => 1, + 'layout' => 'row', + 'button_label' => __('Add Card', 'flynt'), + 'sub_fields' => [ + [ + 'label' => __('Background Image', 'flynt'), + 'name' => 'image', + 'type' => 'image', + 'preview_size' => 'medium', + 'mime_types' => 'jpg,jpeg,png,webp', + 'required' => 1, + ], + [ + 'label' => __('Title', 'flynt'), + 'name' => 'title', + 'type' => 'text', + 'required' => 1, + ], + [ + 'label' => __('Excerpt', 'flynt'), + 'name' => 'subtitle', + 'type' => 'textarea', + 'rows' => 2, + 'instructions' => __('A short line, roughly 15-20 words.', 'flynt'), + ], + [ + 'label' => __('Link', 'flynt'), + 'name' => 'link', + 'type' => 'url', + 'instructions' => __('Only used when "Enable Links" is on in the Options tab.', 'flynt'), + ], + ] + ], $sourceIsField('manual')), + array_merge([ + 'label' => __('Items', 'flynt'), + 'instructions' => __('Pick which posts, pages, or other content to show as cards.', 'flynt'), + 'name' => 'items', + 'type' => 'post_object', + 'post_type' => $itemPostTypes, + 'multiple' => 1, + 'return_format' => 'object', + 'ui' => 1, + 'required' => 1, + ], $sourceIsField('items')), + [ + 'label' => __('Options', 'flynt'), + 'name' => 'optionsTab', + 'type' => 'tab', + 'placement' => 'top', + 'endpoint' => 0 + ], + [ + 'label' => '', + 'name' => 'options', + 'type' => 'group', + 'layout' => 'row', + 'sub_fields' => [ + FieldVariables\getTheme(), + [ + 'label' => __('Enable Links', 'flynt'), + 'instructions' => __('When a card is expanded, its title links to the linked post/page/item (or the manual link, in Manual mode).', 'flynt'), + 'name' => 'enableLinks', + 'type' => 'true_false', + 'default_value' => 0, + 'ui' => 1, + ], + [ + 'label' => __('Animation Speed', 'flynt'), + 'instructions' => __('Duration of the expand/collapse transition, in milliseconds. Lower is faster.', 'flynt'), + 'name' => 'animationSpeed', + 'type' => 'number', + 'default_value' => 700, + 'min' => 100, + 'max' => 2000, + 'step' => 50, + ], + ] + ] + ] + ]; +} diff --git a/Components/ShowcaseExpandingCards/index.twig b/Components/ShowcaseExpandingCards/index.twig new file mode 100644 index 00000000..c95f0445 --- /dev/null +++ b/Components/ShowcaseExpandingCards/index.twig @@ -0,0 +1,51 @@ + +
+ {% if subtitle or title %} +
+ {% if subtitle %} + {{ subtitle|e }} + {% endif %} + {% if title %} +

{{ title|e }}

+ {% endif %} +
+ {% endif %} + + {% if cards %} +
+ {% for card in cards %} + {% set imageSrc = card.image.src starts with 'data:' ? card.image.src : card.image.src|resizeDynamic(900, 1200, 'center') %} +
+
+ +
+
+
+ {% if options.enableLinks and card.link %} + {{ card.title|e }} + {% else %} + {{ card.title|e }} + {% endif %} +
+ {% if card.subtitle %} +
{{ card.subtitle|e }}
+ {% endif %} +
+
+
+ {% endfor %} +
+ {% endif %} +
+
diff --git a/Components/ShowcaseExpandingCards/script.js b/Components/ShowcaseExpandingCards/script.js new file mode 100644 index 00000000..306ffe31 --- /dev/null +++ b/Components/ShowcaseExpandingCards/script.js @@ -0,0 +1,65 @@ +export default function (el) { + const options = Array.from(el.querySelectorAll('[data-ref="option"]')) + if (options.length === 0) return + + const supportsHover = window.matchMedia('(hover: hover) and (pointer: fine)').matches + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + + const activate = (option) => { + if (option.classList.contains('active')) return + options.forEach(opt => opt.classList.remove('active')) + option.classList.add('active') + } + + const onClick = (e) => { + activate(e.currentTarget) + } + + const onMouseEnter = (e) => { + activate(e.currentTarget) + } + + const onKeydown = (e) => { + if (e.target !== e.currentTarget) return + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + activate(e.currentTarget) + } + } + + // Holo Card: only visible (via CSS, gated on .active:hover) once a card is + // already expanded, so this just keeps the pointer position ready for it. + const onPointerMove = (e) => { + const option = e.currentTarget + const rect = option.getBoundingClientRect() + const x = (e.clientX - rect.left) / rect.width + const y = (e.clientY - rect.top) / rect.height + + option.style.setProperty('--expanding-cards-mx', `${x * 100}%`) + option.style.setProperty('--expanding-cards-my', `${y * 100}%`) + } + + options.forEach(option => { + option.addEventListener('click', onClick) + option.addEventListener('keydown', onKeydown) + if (supportsHover) { + option.addEventListener('mouseenter', onMouseEnter) + } + if (supportsHover && !reducedMotion) { + option.addEventListener('pointermove', onPointerMove) + } + }) + + return () => { + options.forEach(option => { + option.removeEventListener('click', onClick) + option.removeEventListener('keydown', onKeydown) + if (supportsHover) { + option.removeEventListener('mouseenter', onMouseEnter) + } + if (supportsHover && !reducedMotion) { + option.removeEventListener('pointermove', onPointerMove) + } + }) + } +} diff --git a/inc/fieldGroups/pageComponents.php b/inc/fieldGroups/pageComponents.php index 8c9a4048..23f163db 100644 --- a/inc/fieldGroups/pageComponents.php +++ b/inc/fieldGroups/pageComponents.php @@ -23,6 +23,7 @@ Components\BlockWysiwyg\getACFLayout(), Components\GridImageText\getACFLayout(), Components\GridPostsLatest\getACFLayout(), + Components\GridSuperPost\getACFLayout(), Components\ListComponents\getACFLayout(), Components\SliderImages\getACFLayout(), Components\ReusableComponent\getACFLayout(), diff --git a/inc/fieldGroups/reusableComponents.php b/inc/fieldGroups/reusableComponents.php index 10260d71..841bb7bb 100644 --- a/inc/fieldGroups/reusableComponents.php +++ b/inc/fieldGroups/reusableComponents.php @@ -23,6 +23,7 @@ Components\BlockWysiwyg\getACFLayout(), Components\GridImageText\getACFLayout(), Components\GridPostsLatest\getACFLayout(), + Components\GridSuperPost\getACFLayout(), Components\ListComponents\getACFLayout(), Components\SliderImages\getACFLayout(), ],