diff --git a/CHANGELOG.md b/CHANGELOG.md index 8374ceb..88fb9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Peptide Starter Theme - Changelog +## [2.2.1] - 2026-04-29 — RCT inline abbreviation tooltip + +### Added + +- `.pr-abbr` tap-target on the term "RCT" in the Established verdict card on + the verdict legend strip (homepage). Tapping/clicking opens a small inline + popover with the full definition: "Randomized Controlled Trial — the gold + standard for clinical evidence." Closes on outside click, Escape key, or + focus leaving the element. +- `assets/js/abbr-tooltip.js` — new JS module (~130 lines) for popover open/ + close, outside-click dismissal, keyboard accessibility (Enter/Space/Escape), + and ARIA attributes (`aria-expanded`, `aria-describedby`, `role="tooltip"`). + Enqueued on front page only (`is_front_page()`). +- `peptide_starter_render_desc( string $text, array $abbrs ): string` helper in + `inc/verdict-helpers.php`. Builds safe HTML from `esc_html`/`esc_attr` + components; verdict taxonomy items carry an optional `abbrs` map. +- `.pr-abbr` and `.pr-abbr-popover` CSS in `style.css` — dashed underline + affordance, fade-in animation, dark-mode-safe surface variables. + +### Fixed + +- `PEPTIDE_STARTER_VERSION` constant in `functions.php` was stale at + `2.0.0-alpha.3`; realigned to `2.2.1` to match `style.css` Version header. + ## [2.1.8] - 2026-04-28 — Homepage verdict section spacing fix ### Fixed diff --git a/assets/js/abbr-tooltip.js b/assets/js/abbr-tooltip.js new file mode 100644 index 0000000..0284868 --- /dev/null +++ b/assets/js/abbr-tooltip.js @@ -0,0 +1,134 @@ +/** + * abbr-tooltip.js — Inline abbreviation popover for verdict legend screen. + * + * What: Opens a small popover anchored to any .pr-abbr element when tapped, + * clicked, or activated via keyboard. Closes on outside click or Escape. + * Who triggers: Enqueued by functions.php on is_front_page() requests only. + * Depends on: .pr-abbr elements in the DOM with data-def attributes; CSS in + * style.css (.pr-abbr-popover). + * + * @see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr elements + * @see style.css — .pr-abbr, .pr-abbr-popover visual styles + * @package peptide-starter + */ + +( function () { + 'use strict'; + + var POPOVER_CLASS = 'pr-abbr-popover'; + var ACTIVE_CLASS = 'pr-abbr--active'; + var activePopover = null; + var activeAbbr = null; + + /** + * Remove the currently open popover, if any. + * + * @return {void} + */ + function closePopover() { + if ( activePopover ) { + activePopover.remove(); + activePopover = null; + } + if ( activeAbbr ) { + activeAbbr.classList.remove( ACTIVE_CLASS ); + activeAbbr.removeAttribute( 'aria-expanded' ); + activeAbbr = null; + } + } + + /** + * Create and position a popover anchored to the given abbr element. + * + * @param {HTMLElement} abbr - The .pr-abbr element that was activated. + * @return {void} + */ + function openPopover( abbr ) { + closePopover(); + + var def = abbr.getAttribute( 'data-def' ); + if ( ! def ) { + return; + } + + var popover = document.createElement( 'div' ); + popover.className = POPOVER_CLASS; + popover.textContent = def; + popover.setAttribute( 'role', 'tooltip' ); + popover.setAttribute( 'id', POPOVER_CLASS + '-' + Date.now() ); + + document.body.appendChild( popover ); + + // Position below the abbr, clamped to viewport width. + var rect = abbr.getBoundingClientRect(); + var scrollTop = window.pageYOffset || document.documentElement.scrollTop; + var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + var top = rect.bottom + scrollTop + 6; + var left = rect.left + scrollLeft; + var maxLeft = window.innerWidth - popover.offsetWidth - 16; + + popover.style.top = top + 'px'; + popover.style.left = Math.max( 8, Math.min( left, maxLeft ) ) + 'px'; + + abbr.classList.add( ACTIVE_CLASS ); + abbr.setAttribute( 'aria-expanded', 'true' ); + abbr.setAttribute( 'aria-describedby', popover.id ); + + activePopover = popover; + activeAbbr = abbr; + } + + /** + * Toggle the popover for an abbr element (open if closed, close if open). + * + * @param {HTMLElement} abbr - The .pr-abbr element that was activated. + * @return {void} + */ + function togglePopover( abbr ) { + if ( activeAbbr === abbr ) { + closePopover(); + } else { + openPopover( abbr ); + } + } + + // ------------------------------------------------------------------------- + // Event listeners + // ------------------------------------------------------------------------- + + document.addEventListener( 'click', function ( e ) { + var abbr = e.target.closest( '.pr-abbr' ); + if ( abbr ) { + e.stopPropagation(); + togglePopover( abbr ); + return; + } + // Click outside any abbr — close. + closePopover(); + } ); + + document.addEventListener( 'keydown', function ( e ) { + if ( e.key === 'Escape' ) { + closePopover(); + return; + } + if ( e.key === 'Enter' || e.key === ' ' ) { + var abbr = e.target.closest( '.pr-abbr' ); + if ( abbr ) { + e.preventDefault(); + togglePopover( abbr ); + } + } + } ); + + // Close when focus leaves the popover+abbr pair entirely. + document.addEventListener( 'focusin', function ( e ) { + if ( + activeAbbr && + ! activeAbbr.contains( e.target ) && + ( ! activePopover || ! activePopover.contains( e.target ) ) + ) { + closePopover(); + } + } ); +} )(); diff --git a/footer.php b/footer.php index e5b2bbe..ee06f41 100644 --- a/footer.php +++ b/footer.php @@ -57,7 +57,7 @@ echo '

' . esc_html__( 'Our Method', 'peptide-starter' ) . '

'; echo ''; } diff --git a/front-page.php b/front-page.php index 79745bf..ffcfc31 100644 --- a/front-page.php +++ b/front-page.php @@ -101,7 +101,7 @@ $featured_monograph_ids = array( 211, 36, 177 ); -

+

diff --git a/functions.php b/functions.php index 3c090fc..c594ecb 100644 --- a/functions.php +++ b/functions.php @@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Theme constants. -define( 'PEPTIDE_STARTER_VERSION', '2.0.0-alpha.3' ); +define( 'PEPTIDE_STARTER_VERSION', '2.2.1' ); define( 'PEPTIDE_STARTER_DIR', get_template_directory() ); define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() ); @@ -88,6 +88,11 @@ function peptide_starter_scripts() { wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true ); wp_enqueue_script( 'peptide-starter-settings-panel', PEPTIDE_STARTER_URI . '/assets/js/settings-panel.js', array(), PEPTIDE_STARTER_VERSION, true ); + // Abbreviation popovers — only needed on the front page verdict legend strip. + if ( is_front_page() ) { + wp_enqueue_script( 'peptide-starter-abbr-tooltip', PEPTIDE_STARTER_URI . '/assets/js/abbr-tooltip.js', array(), PEPTIDE_STARTER_VERSION, true ); + } + wp_localize_script( 'peptide-starter-theme', 'peptideStarterData', diff --git a/inc/verdict-helpers.php b/inc/verdict-helpers.php index 7013d5d..772c162 100644 --- a/inc/verdict-helpers.php +++ b/inc/verdict-helpers.php @@ -19,7 +19,7 @@ * * Canonical order matches CMO brief §5.1 and the 5-state enum in verdict-meta.php. * - * @return array[] Each element: state (string), glyph (string), label (string), description (string). + * @return array[] Each element: state (string), glyph (string), label (string), description (string), abbrs (array, optional). */ function peptide_starter_get_verdict_taxonomy() { return array( @@ -28,6 +28,10 @@ function peptide_starter_get_verdict_taxonomy() { 'glyph' => '✓', 'label' => 'Established', 'description' => 'Strong RCT evidence; widely prescribed or validated.', + // abbrs: terms in description that should render as inline tap-targets. + 'abbrs' => array( + 'RCT' => 'Randomized Controlled Trial — the gold standard for clinical evidence. Participants are randomly assigned to treatment or control groups to measure real effect.', + ), ), array( 'state' => 'promising', @@ -88,3 +92,46 @@ function peptide_starter_get_verdict_config() { ), ); } + +/** + * Renders a description string with optional inline abbreviation tap-targets. + * + * What: Builds safe HTML for a verdict card description, replacing known + * abbreviation terms with elements that trigger a tooltip. + * Who calls it: front-page.php verdict explainer loop. + * Depends on: Nothing — pure string transformation. + * + * The output is intentionally built from escaped components (not from raw $text) + * so we never expose unescaped user or data-layer content. Each substitution is: + * esc_html( $text ) → str_replace( literal_term, safe_abbr_html ) + * This is safe because the abbr tag, class, and data-def are all hardcoded or + * escaped here, and the term key is a literal constant from our own data array. + * + * @param string $text Plain-text description string. Will be HTML-escaped. + * @param array $abbrs Associative array of term => definition. Keys are literal + * strings (e.g. 'RCT'). Values are plain-text definitions. + * @return string Safe HTML string suitable for echo without further escaping. + * + * @see inc/verdict-helpers.php — $abbrs data lives in peptide_starter_get_verdict_taxonomy() + * @see assets/js/abbr-tooltip.js — JS that handles tap/click to show the popover + * @see style.css — .pr-abbr and .pr-abbr-popover styles + */ +function peptide_starter_render_desc( string $text, array $abbrs ): string { + $html = esc_html( $text ); + + foreach ( $abbrs as $term => $definition ) { + $safe_term = esc_html( $term ); + $safe_def = esc_attr( $definition ); + $abbr_html = sprintf( + '%s', + $safe_def, + $safe_term, + $safe_def, + $safe_term + ); + // Replace only the first occurrence — abbreviations should not stack. + $html = preg_replace( '/' . preg_quote( $safe_term, '/' ) . '/', $abbr_html, $html, 1 ); + } + + return $html; +} diff --git a/style.css b/style.css index bce5998..f34cfec 100644 --- a/style.css +++ b/style.css @@ -4,7 +4,7 @@ Theme URI: https://github.com/peptiderepo/peptide-starter Description: Premium, accessible WordPress theme for scientific peptide reference database Author: Peptide Repo Author URI: https://peptiderepo.com -Version: 2.2.0 +Version: 2.2.1 License: GPL v2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Text Domain: peptide-starter @@ -3410,3 +3410,55 @@ html[data-theme="dark"] .ps-verify-required { grid-template-columns: repeat(5, 1fr); } } + +/* ========================================================================== + Abbreviation tooltip (.pr-abbr / .pr-abbr-popover) + Inline tap-targets for glossary terms in the verdict legend strip. + @see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr + @see assets/js/abbr-tooltip.js — JS popover logic + ========================================================================== */ + +.pr-abbr { + border-bottom: 1px dashed currentColor; + cursor: help; + text-decoration: none; + white-space: nowrap; + /* Slightly muted so the dashed line reads as "interactive" not "broken link". */ + opacity: 0.85; + transition: opacity 0.15s ease; +} + +.pr-abbr:hover, +.pr-abbr:focus, +.pr-abbr--active { + opacity: 1; + outline: none; +} + +.pr-abbr-popover { + position: absolute; + z-index: 9999; + max-width: min( 280px, calc( 100vw - 32px ) ); + padding: 0.6rem 0.85rem; + border-radius: 6px; + font-size: 0.8125rem; + line-height: 1.5; + /* Use theme surface colours so dark-mode inherits correctly. */ + background-color: var(--color-surface-elevated, #1e1e2e); + color: var(--color-text-primary, #e2e2f0); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + /* Subtle fade-in. */ + animation: pr-popover-in 0.12s ease forwards; +} + +@keyframes pr-popover-in { + from { + opacity: 0; + transform: translateY( -4px ); + } + to { + opacity: 1; + transform: translateY( 0 ); + } +} +