feat: RCT inline abbreviation tooltip on verdict legend strip (v2.2.1) (#53)
CI green (PHP Lint 8.1/8.2/8.3 ✓, PHPCS ✓, PHPUnit 8.1/8.2/8.3 ✓). QA: PASS.
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
|||||||
# Peptide Starter Theme - Changelog
|
# 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
|
## [2.1.8] - 2026-04-28 — Homepage verdict section spacing fix
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
134
assets/js/abbr-tooltip.js
Normal file
134
assets/js/abbr-tooltip.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} )();
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
echo '<h3>' . esc_html__( 'Our Method', 'peptide-starter' ) . '</h3>';
|
echo '<h3>' . esc_html__( 'Our Method', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<ul>';
|
echo '<ul>';
|
||||||
echo '<li><a href="' . esc_url( home_url( '/how-we-review-peptides' ) ) . '">' . esc_html__( 'How We Review Peptides', 'peptide-starter' ) . '</a></li>';
|
echo '<li><a href="' . esc_url( home_url( '/how-we-review-peptides' ) ) . '">' . esc_html__( 'How We Review Peptides', 'peptide-starter' ) . '</a></li>';
|
||||||
echo '<li><a href="' . esc_url( home_url( '/our-methodology' ) ) . '">' . esc_html__( 'Our Methodology', 'peptide-starter' ) . '</a></li>';
|
echo '<li><a href="' . esc_url( home_url( '/our-methodology' ) ) . '">' . esc_html__( 'Our Methodology', 'peptide-starter' ) . '</a></li>';
|
||||||
echo '<li><a href="' . esc_url( home_url( '/about' ) ) . '">' . esc_html__( 'Editorial Standards', 'peptide-starter' ) . '</a></li>';
|
echo '<li><a href="' . esc_url( home_url( '/about' ) ) . '">' . esc_html__( 'Editorial Standards', 'peptide-starter' ) . '</a></li>';
|
||||||
echo '</ul>';
|
echo '</ul>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ $featured_monograph_ids = array( 211, 36, 177 );
|
|||||||
<span class="pr-verdict__glyph" aria-hidden="true"><?php echo esc_html( $v['glyph'] ); ?></span>
|
<span class="pr-verdict__glyph" aria-hidden="true"><?php echo esc_html( $v['glyph'] ); ?></span>
|
||||||
<span class="pr-verdict__label"><?php echo esc_html( $v['label'] ); ?></span>
|
<span class="pr-verdict__label"><?php echo esc_html( $v['label'] ); ?></span>
|
||||||
</span>
|
</span>
|
||||||
<p class="hero-verdict-explainer__desc"><?php echo esc_html( $v['description'] ); ?></p>
|
<p class="hero-verdict-explainer__desc"><?php echo peptide_starter_render_desc( $v['description'], $v['abbrs'] ?? array() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output is built from esc_html/esc_attr components inside render_desc(). ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Theme constants.
|
// 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_DIR', get_template_directory() );
|
||||||
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
|
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-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 );
|
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(
|
wp_localize_script(
|
||||||
'peptide-starter-theme',
|
'peptide-starter-theme',
|
||||||
'peptideStarterData',
|
'peptideStarterData',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*
|
*
|
||||||
* Canonical order matches CMO brief §5.1 and the 5-state enum in verdict-meta.php.
|
* 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() {
|
function peptide_starter_get_verdict_taxonomy() {
|
||||||
return array(
|
return array(
|
||||||
@@ -28,6 +28,10 @@ function peptide_starter_get_verdict_taxonomy() {
|
|||||||
'glyph' => '✓',
|
'glyph' => '✓',
|
||||||
'label' => 'Established',
|
'label' => 'Established',
|
||||||
'description' => 'Strong RCT evidence; widely prescribed or validated.',
|
'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(
|
array(
|
||||||
'state' => 'promising',
|
'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 <abbr> 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(
|
||||||
|
'<abbr class="pr-abbr" data-def="%s" tabindex="0" role="button" aria-label="%s: %s">%s</abbr>',
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|||||||
54
style.css
54
style.css
@@ -4,7 +4,7 @@ Theme URI: https://github.com/peptiderepo/peptide-starter
|
|||||||
Description: Premium, accessible WordPress theme for scientific peptide reference database
|
Description: Premium, accessible WordPress theme for scientific peptide reference database
|
||||||
Author: Peptide Repo
|
Author: Peptide Repo
|
||||||
Author URI: https://peptiderepo.com
|
Author URI: https://peptiderepo.com
|
||||||
Version: 2.2.0
|
Version: 2.2.1
|
||||||
License: GPL v2 or later
|
License: GPL v2 or later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
Text Domain: peptide-starter
|
Text Domain: peptide-starter
|
||||||
@@ -3410,3 +3410,55 @@ html[data-theme="dark"] .ps-verify-required {
|
|||||||
grid-template-columns: repeat(5, 1fr);
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user