Compare commits
33 Commits
v2.0.0
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d417299e05 | ||
|
|
6a288262c6 | ||
|
|
a751a8f890 | ||
|
|
601768a1b1 | ||
|
|
69fd0f643f | ||
|
|
0a2a48a9f0 | ||
|
|
844951b268 | ||
|
|
2b23bb7e2a | ||
|
|
bdc3ace6cf | ||
|
|
6f85210950 | ||
|
|
33faab7c4f | ||
|
|
394df82bbe | ||
|
|
89a3ed66d3 | ||
|
|
a7aad01af7 | ||
|
|
4fa63e43cc | ||
|
|
fb09e50c02 | ||
|
|
dbc3c01bc4 | ||
|
|
fdaea84d72 | ||
|
|
22716b4227 | ||
|
|
03bedf1d6b | ||
|
|
3625c25655 | ||
|
|
e4bf5faf5b | ||
|
|
95985d5d30 | ||
|
|
0cbeb91a77 | ||
|
|
2b25d7d872 | ||
|
|
0cb4247b1a | ||
|
|
27d9cc5831 | ||
|
|
f20a598bac | ||
|
|
e3ab80f482 | ||
|
|
aecb4bd1cb | ||
|
|
08dc8480ba | ||
|
|
3942c6b29e | ||
|
|
4188bba014 |
120
.github/workflows/ci.yml
vendored
120
.github/workflows/ci.yml
vendored
@@ -1,112 +1,24 @@
|
|||||||
name: CI — Lint & Test
|
name: CI
|
||||||
|
|
||||||
# Runs on every PR and every push to main.
|
|
||||||
# Three jobs in parallel:
|
|
||||||
# 1. PHP Lint — syntax check across supported PHP versions
|
|
||||||
# 2. PHPCS — WordPress coding standards (non-blocking during theme rollout)
|
|
||||||
# 3. PHPUnit — runs the theme's test suite against the WordPress test framework
|
|
||||||
|
|
||||||
|
# Thin caller — delegates all jobs to the shared reusable workflow.
|
||||||
|
# Must include workflow_call so deploy.yml can gate on this workflow.
|
||||||
on:
|
on:
|
||||||
|
pull_request: {}
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
workflow_call: {}
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
|
# Required by the reusable phpcs job which commits phpcbf auto-fixes back
|
||||||
|
# to the PR branch. GitHub enforces that callers declare at least the same
|
||||||
|
# permissions as any job-level override in the called workflow.
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
php-lint:
|
ci:
|
||||||
name: PHP Lint (${{ matrix.php }})
|
uses: peptide-repo/peptide-e2e/.github/workflows/ci.yml@main
|
||||||
runs-on: ubuntu-latest
|
with:
|
||||||
strategy:
|
tests: wp-framework
|
||||||
fail-fast: false
|
has_js: true
|
||||||
matrix:
|
line_limit_paths: inc
|
||||||
php: ['7.4', '8.1', '8.3']
|
secrets: inherit
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup PHP ${{ matrix.php }}
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
coverage: none
|
|
||||||
- name: Syntax check all PHP files
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
find . -name '*.php' \
|
|
||||||
-not -path './vendor/*' \
|
|
||||||
-not -path './node_modules/*' \
|
|
||||||
-not -path './.git/*' \
|
|
||||||
-print0 | xargs -0 -n1 -P4 php -l > /dev/null
|
|
||||||
|
|
||||||
phpcs:
|
|
||||||
name: PHPCS (WordPress standards)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: '8.1'
|
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-progress --no-interaction --prefer-dist
|
|
||||||
- name: Run PHPCS
|
|
||||||
run: vendor/bin/phpcs --standard=.phpcs.xml.dist
|
|
||||||
|
|
||||||
phpunit:
|
|
||||||
name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wp }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
php: ['8.1']
|
|
||||||
wp: ['latest']
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
env:
|
|
||||||
MYSQL_ROOT_PASSWORD: root
|
|
||||||
MYSQL_DATABASE: wp_tests
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install subversion
|
|
||||||
# ubuntu-latest no longer ships svn preinstalled. The WP test suite
|
|
||||||
# bootstrap script checks out develop.svn.wordpress.org via svn.
|
|
||||||
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends subversion
|
|
||||||
- name: Setup PHP ${{ matrix.php }}
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
coverage: none
|
|
||||||
tools: composer:v2
|
|
||||||
extensions: mysqli
|
|
||||||
- name: Install composer dependencies
|
|
||||||
run: composer install --no-progress --no-interaction --prefer-dist
|
|
||||||
- name: Wait for MySQL
|
|
||||||
run: |
|
|
||||||
for i in {1..30}; do
|
|
||||||
if mysqladmin ping -h 127.0.0.1 -P 3306 -u root -proot --silent; then
|
|
||||||
echo "MySQL is up"; break
|
|
||||||
fi
|
|
||||||
echo "Waiting for MySQL..."; sleep 2
|
|
||||||
done
|
|
||||||
- name: Install WordPress test suite
|
|
||||||
env:
|
|
||||||
WP_TESTS_DIR: /tmp/wordpress-tests-lib
|
|
||||||
# Trailing slash matters: the bootstrap config substitutes this verbatim into ABSPATH,
|
|
||||||
# then concatenates 'wp-settings.php'. Without the slash you get '/tmp/wordpresswp-settings.php'.
|
|
||||||
WP_CORE_DIR: /tmp/wordpress/
|
|
||||||
run: |
|
|
||||||
chmod +x bin/install-wp-tests.sh
|
|
||||||
# 6th arg = skip-database-creation: the MySQL service already created wp_tests via MYSQL_DATABASE env.
|
|
||||||
bin/install-wp-tests.sh wp_tests root root 127.0.0.1:3306 ${{ matrix.wp }} true
|
|
||||||
- name: Run PHPUnit
|
|
||||||
env:
|
|
||||||
WP_TESTS_DIR: /tmp/wordpress-tests-lib
|
|
||||||
run: vendor/bin/phpunit --colors=always
|
|
||||||
|
|||||||
314
.github/workflows/cto-review.yml
vendored
314
.github/workflows/cto-review.yml
vendored
@@ -1,20 +1,17 @@
|
|||||||
name: CTO Review
|
name: CTO Review
|
||||||
|
|
||||||
# Automated PR review powered by Claude + DeepSeek + Kimi (via OpenRouter)
|
# Automated PR review powered by Claude Haiku (via OpenRouter).
|
||||||
# plus Neurometric PR-Summarizer (a Qwen3-4B task-specialist SLM).
|
|
||||||
#
|
#
|
||||||
# During the A/B window (until further notice), every PR gets reviewed by FOUR models
|
# A/B panel decision 2026-06-12: panel dropped to Haiku-only.
|
||||||
# in parallel. The PR comment shows each model's verdict side-by-side so we can compare
|
# Evidence: convo/cto/threads/2026-06-qa-ab-review/02-analyst-findings.md
|
||||||
# quality/cost on real PRs before picking a permanent default.
|
# DeepSeek: high false-positive rate (absint()="code exec", PLUGIN_DIR="dir traversal").
|
||||||
|
# Kimi: ~60% model failure rate across 13 PRs sampled.
|
||||||
|
# Neurometric: summarizer only, zero unique-real findings, unreliable heuristic verdict.
|
||||||
|
# Haiku: 0 failures, lowest noise, equivalent real-signal catch rate vs 4-voice panel.
|
||||||
#
|
#
|
||||||
# Neurometric is a "prose voice": it does not support OpenAI tool-calling, so its
|
# Does NOT merge. Posts formal APPROVE/REQUEST_CHANGES/COMMENT reviews.
|
||||||
# contribution is raw markdown + a heuristic verdict. It is never the primary voice.
|
|
||||||
#
|
#
|
||||||
# Does NOT merge. Does NOT post formal APPROVE/REQUEST_CHANGES reviews — always COMMENT
|
# Requires repo secret: OPENROUTER_API_KEY
|
||||||
# (GITHUB_TOKEN can't formally approve PRs without repo-setting toggle).
|
|
||||||
#
|
|
||||||
# Requires repo secrets: OPENROUTER_API_KEY, NEUROMETRIC_API_KEY (optional; the
|
|
||||||
# Neurometric voice self-skips with an inline error note if the key is missing).
|
|
||||||
#
|
#
|
||||||
# Cost cap: tracked in Repository Variables (CTO_REVIEW_SPEND_CENTS, CTO_REVIEW_CAP_CENTS).
|
# Cost cap: tracked in Repository Variables (CTO_REVIEW_SPEND_CENTS, CTO_REVIEW_CAP_CENTS).
|
||||||
# Default cap is 1000 cents ($10/month). Auto-resets on the 1st of each month.
|
# Default cap is 1000 cents ($10/month). Auto-resets on the 1st of each month.
|
||||||
@@ -123,17 +120,18 @@ jobs:
|
|||||||
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=50
|
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=50
|
||||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||||
DIFF=$(git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000)
|
# Write the diff to a file, not a step output / env var. Passing a
|
||||||
|
# ~400KB diff through the process env (execve argv+envp) exceeds the
|
||||||
|
# runner ARG_MAX and fails the next step with "Argument list too long".
|
||||||
|
# File transport also sidesteps the ~1MB step-output cap.
|
||||||
|
git diff --no-color "$BASE_SHA" "$HEAD_SHA" | head -c 400000 > /tmp/cto-review-diff.txt
|
||||||
FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
||||||
SIZE=$(echo -n "$DIFF" | wc -c)
|
SIZE=$(wc -c < /tmp/cto-review-diff.txt)
|
||||||
{
|
{
|
||||||
echo "size=$SIZE"
|
echo "size=$SIZE"
|
||||||
echo "files<<EOF_FILES"
|
echo "files<<EOF_FILES"
|
||||||
echo "$FILES"
|
echo "$FILES"
|
||||||
echo "EOF_FILES"
|
echo "EOF_FILES"
|
||||||
echo "diff<<EOF_DIFF"
|
|
||||||
echo "$DIFF"
|
|
||||||
echo "EOF_DIFF"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Load repo conventions
|
- name: Load repo conventions
|
||||||
@@ -150,18 +148,17 @@ jobs:
|
|||||||
echo "EOF_CONV"
|
echo "EOF_CONV"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Call 4 models in parallel (OpenRouter + Neurometric)
|
- name: Call Haiku via OpenRouter
|
||||||
if: steps.budget.outputs.skip != 'true'
|
if: steps.budget.outputs.skip != 'true'
|
||||||
id: review
|
id: review
|
||||||
env:
|
env:
|
||||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||||
NEUROMETRIC_API_KEY: ${{ secrets.NEUROMETRIC_API_KEY }}
|
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
PR_BODY: ${{ github.event.pull_request.body }}
|
PR_BODY: ${{ github.event.pull_request.body }}
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
REPO_NAME: ${{ github.event.repository.name }}
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
DIFF: ${{ steps.diff.outputs.diff }}
|
DIFF_PATH: /tmp/cto-review-diff.txt
|
||||||
DIFF_SIZE: ${{ steps.diff.outputs.size }}
|
DIFF_SIZE: ${{ steps.diff.outputs.size }}
|
||||||
CHANGED_FILES: ${{ steps.diff.outputs.files }}
|
CHANGED_FILES: ${{ steps.diff.outputs.files }}
|
||||||
CONVENTIONS: ${{ steps.conventions.outputs.content }}
|
CONVENTIONS: ${{ steps.conventions.outputs.content }}
|
||||||
@@ -183,24 +180,17 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import json, os, sys, urllib.request, concurrent.futures
|
import json, os, sys, urllib.request
|
||||||
|
|
||||||
api_key = os.environ["OPENROUTER_API_KEY"]
|
api_key = os.environ["OPENROUTER_API_KEY"]
|
||||||
|
|
||||||
# A/B harness: four models. Each gets the same prompt.
|
# Haiku-only reviewer. A/B panel dropped 2026-06-12 per QA data:
|
||||||
# Primary model's verdict drives the heading. Others are labeled.
|
# DeepSeek high false-positive rate, Kimi ~60% failure rate,
|
||||||
# Edit this list to change the roster (or drop to 1 after A/B window closes).
|
# Neurometric zero unique-real findings. Haiku retained as sole voice.
|
||||||
#
|
MODEL_SLUG = "anthropic/claude-haiku-4.5"
|
||||||
# provider_type semantics:
|
MODEL_DISPLAY = "Claude Haiku 4.5"
|
||||||
# - "openrouter_tools": OpenAI tool-calling via OpenRouter; structured verdict.
|
INPUT_PER_M_USD = 1.00
|
||||||
# - "neurometric_prose": Neurometric marketplace SLM; prose output + heuristic verdict.
|
OUTPUT_PER_M_USD = 5.00
|
||||||
MODELS = [
|
|
||||||
# (slug, display name, input_per_M_USD, output_per_M_USD, is_primary, provider_type)
|
|
||||||
("anthropic/claude-haiku-4.5", "Claude Haiku 4.5", 1.00, 5.00, True, "openrouter_tools"),
|
|
||||||
("deepseek/deepseek-chat-v3.1", "DeepSeek V3.1", 0.15, 0.75, False, "openrouter_tools"),
|
|
||||||
("moonshotai/kimi-k2-thinking", "Kimi K2 Thinking", 0.60, 2.50, False, "openrouter_tools"),
|
|
||||||
("neurometric/pr-summarizer", "Neurometric PR-Summarizer (Qwen3-4B)", 0.00, 0.00, False, "neurometric_prose"),
|
|
||||||
]
|
|
||||||
|
|
||||||
system = """You are the CTO and lead reviewer for the peptiderepo project. You are reviewing a pull request opened by an autonomous agent or human contributor.
|
system = """You are the CTO and lead reviewer for the peptiderepo project. You are reviewing a pull request opened by an autonomous agent or human contributor.
|
||||||
|
|
||||||
@@ -229,7 +219,9 @@ jobs:
|
|||||||
user_blocks.append(f"**Changed files:**\n```\n{os.environ['CHANGED_FILES']}\n```\n")
|
user_blocks.append(f"**Changed files:**\n```\n{os.environ['CHANGED_FILES']}\n```\n")
|
||||||
if os.environ.get("CONVENTIONS", "").strip():
|
if os.environ.get("CONVENTIONS", "").strip():
|
||||||
user_blocks.append(f"# Repo CONVENTIONS.md\n{os.environ['CONVENTIONS']}\n")
|
user_blocks.append(f"# Repo CONVENTIONS.md\n{os.environ['CONVENTIONS']}\n")
|
||||||
user_blocks.append(f"# Diff\n```diff\n{os.environ['DIFF']}\n```\n")
|
with open(os.environ["DIFF_PATH"], encoding="utf-8", errors="replace") as _df:
|
||||||
|
_diff_text = _df.read()
|
||||||
|
user_blocks.append(f"# Diff\n```diff\n{_diff_text}\n```\n")
|
||||||
user_message = "\n\n".join(user_blocks)
|
user_message = "\n\n".join(user_blocks)
|
||||||
|
|
||||||
tools = [{
|
tools = [{
|
||||||
@@ -261,218 +253,100 @@ jobs:
|
|||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
PER_MODEL_TIMEOUT = 60 # seconds; one hung model should not block siblings
|
body = {
|
||||||
|
"model": MODEL_SLUG,
|
||||||
|
"max_tokens": 2048,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system},
|
||||||
|
{"role": "user", "content": user_message}
|
||||||
|
],
|
||||||
|
"tools": tools,
|
||||||
|
"tool_choice": {"type": "function", "function": {"name": "submit_review"}}
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"content-type": "application/json",
|
||||||
|
"HTTP-Referer": "https://github.com/peptiderepo",
|
||||||
|
"X-Title": f"peptiderepo CTO reviewer ({MODEL_DISPLAY})"
|
||||||
|
},
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
|
||||||
# Heuristic verdict inference for prose-voice models that can't do tool-calling.
|
error = None
|
||||||
# Keyword order matters: block markers beat approve markers.
|
review = None
|
||||||
PROSE_BLOCK_MARKERS = [
|
cost_cents = 0.0
|
||||||
"do not merge", "do not approve", "should not be merged", "reject this",
|
ptok = 0
|
||||||
"critical security", "critical issue", "critical vulnerability",
|
ctok = 0
|
||||||
"blocker", "blocking issue", "request_changes", "request changes",
|
|
||||||
"**critical**", "sql injection", "must be addressed before",
|
|
||||||
]
|
|
||||||
PROSE_APPROVE_MARKERS = [
|
|
||||||
"safe to merge", "ready to merge", "no blocking issues", "lgtm",
|
|
||||||
"approve this", "looks good to merge", "no concerns",
|
|
||||||
]
|
|
||||||
|
|
||||||
def infer_verdict_from_prose(text):
|
try:
|
||||||
t = (text or "").lower()
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||||
if any(m in t for m in PROSE_BLOCK_MARKERS):
|
data = json.loads(resp.read())
|
||||||
return "REQUEST_CHANGES"
|
except Exception as e:
|
||||||
if any(m in t for m in PROSE_APPROVE_MARKERS):
|
error = str(e)[:300]
|
||||||
return "APPROVE"
|
data = {}
|
||||||
return "COMMENT"
|
|
||||||
|
|
||||||
def call_openrouter_tools(slug, display, in_rate, out_rate, is_primary):
|
|
||||||
body = {
|
|
||||||
"model": slug,
|
|
||||||
"max_tokens": 2048,
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": system},
|
|
||||||
{"role": "user", "content": user_message}
|
|
||||||
],
|
|
||||||
"tools": tools,
|
|
||||||
"tool_choice": {"type": "function", "function": {"name": "submit_review"}}
|
|
||||||
}
|
|
||||||
req = urllib.request.Request(
|
|
||||||
"https://openrouter.ai/api/v1/chat/completions",
|
|
||||||
data=json.dumps(body).encode(),
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"content-type": "application/json",
|
|
||||||
"HTTP-Referer": "https://github.com/peptiderepo",
|
|
||||||
"X-Title": f"peptiderepo CTO reviewer ({display})"
|
|
||||||
},
|
|
||||||
method="POST"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req, timeout=PER_MODEL_TIMEOUT) as resp:
|
|
||||||
data = json.loads(resp.read())
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
|
||||||
"provider_type": "openrouter_tools",
|
|
||||||
"error": str(e)[:300],
|
|
||||||
"cost_cents": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if not error:
|
||||||
choice = data.get("choices", [{}])[0].get("message", {})
|
choice = data.get("choices", [{}])[0].get("message", {})
|
||||||
tool_calls = choice.get("tool_calls", [])
|
tool_calls = choice.get("tool_calls", [])
|
||||||
if not tool_calls:
|
if not tool_calls:
|
||||||
return {
|
error = f"No tool_call in response. Raw content: {str(choice)[:300]}"
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
else:
|
||||||
"provider_type": "openrouter_tools",
|
try:
|
||||||
"error": f"No tool_call in response. Raw content: {str(choice)[:300]}",
|
review = json.loads(tool_calls[0]["function"]["arguments"])
|
||||||
"cost_cents": 0,
|
except Exception as e:
|
||||||
}
|
error = f"Malformed tool args: {e}"
|
||||||
|
|
||||||
try:
|
if not error:
|
||||||
review = json.loads(tool_calls[0]["function"]["arguments"])
|
usage = data.get("usage", {})
|
||||||
except Exception as e:
|
ptok = usage.get("prompt_tokens", 0)
|
||||||
return {
|
ctok = usage.get("completion_tokens", 0)
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
cost_usd = (ptok * INPUT_PER_M_USD / 1_000_000) + (ctok * OUTPUT_PER_M_USD / 1_000_000)
|
||||||
"provider_type": "openrouter_tools",
|
cost_cents = cost_usd * 100
|
||||||
"error": f"Malformed tool args: {e}",
|
|
||||||
"cost_cents": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
usage = data.get("usage", {})
|
# Build comment
|
||||||
ptok = usage.get("prompt_tokens", 0)
|
if error:
|
||||||
ctok = usage.get("completion_tokens", 0)
|
heading_verdict = "ERROR"
|
||||||
cost_usd = (ptok * in_rate / 1_000_000) + (ctok * out_rate / 1_000_000)
|
lines = [f"## CTO Review — {heading_verdict}", ""]
|
||||||
cost_cents = cost_usd * 100
|
lines.append(f"Model failed: `{error}`")
|
||||||
return {
|
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
|
||||||
"provider_type": "openrouter_tools",
|
|
||||||
"review": review, "prompt_tokens": ptok, "completion_tokens": ctok,
|
|
||||||
"cost_cents": cost_cents,
|
|
||||||
}
|
|
||||||
|
|
||||||
def call_neurometric_prose(slug, display, is_primary):
|
|
||||||
nm_key = os.environ.get("NEUROMETRIC_API_KEY", "")
|
|
||||||
if not nm_key:
|
|
||||||
return {
|
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
|
||||||
"provider_type": "neurometric_prose",
|
|
||||||
"error": "NEUROMETRIC_API_KEY secret not set on this repo",
|
|
||||||
"cost_cents": 0,
|
|
||||||
}
|
|
||||||
body = {
|
|
||||||
"model": slug,
|
|
||||||
"max_tokens": 2048,
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": system},
|
|
||||||
{"role": "user", "content": user_message},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
req = urllib.request.Request(
|
|
||||||
"https://api.neurometric.ai/v1/chat/completions",
|
|
||||||
data=json.dumps(body).encode(),
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {nm_key}",
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
method="POST",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req, timeout=PER_MODEL_TIMEOUT) as resp:
|
|
||||||
data = json.loads(resp.read())
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
|
||||||
"provider_type": "neurometric_prose",
|
|
||||||
"error": str(e)[:300],
|
|
||||||
"cost_cents": 0,
|
|
||||||
}
|
|
||||||
prose = data.get("choices", [{}])[0].get("message", {}).get("content") or ""
|
|
||||||
verdict = infer_verdict_from_prose(prose)
|
|
||||||
return {
|
|
||||||
"slug": slug, "display": display, "is_primary": is_primary,
|
|
||||||
"provider_type": "neurometric_prose",
|
|
||||||
"review": {"verdict": verdict, "prose": prose},
|
|
||||||
"prompt_tokens": 0, "completion_tokens": 0,
|
|
||||||
"cost_cents": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def call_model(slug, display, in_rate, out_rate, is_primary, provider_type="openrouter_tools"):
|
|
||||||
if provider_type == "neurometric_prose":
|
|
||||||
return call_neurometric_prose(slug, display, is_primary)
|
|
||||||
return call_openrouter_tools(slug, display, in_rate, out_rate, is_primary)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(MODELS)) as pool:
|
|
||||||
futures = [pool.submit(call_model, *m) for m in MODELS]
|
|
||||||
for f in concurrent.futures.as_completed(futures):
|
|
||||||
results.append(f.result())
|
|
||||||
|
|
||||||
# Order: primary first, then alphabetical for stable comparison
|
|
||||||
results.sort(key=lambda r: (not r.get("is_primary"), r["display"]))
|
|
||||||
|
|
||||||
# Build combined comment
|
|
||||||
primary = next((r for r in results if r.get("is_primary") and "review" in r), None)
|
|
||||||
heading_verdict = primary["review"]["verdict"] if primary else "MIXED"
|
|
||||||
|
|
||||||
lines = [f"## CTO Review — {heading_verdict}", ""]
|
|
||||||
lines.append(f"_A/B harness active: {len(results)} models reviewing in parallel. Primary verdict drives heading; others are advisory._")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
total_cents = 0.0
|
|
||||||
for r in results:
|
|
||||||
lines.append(f"---")
|
|
||||||
primary_tag = " (primary)" if r.get("is_primary") else ""
|
|
||||||
lines.append(f"### {r['display']}{primary_tag}")
|
|
||||||
if "error" in r:
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"⚠️ Model failed: `{r['error']}`")
|
|
||||||
lines.append("")
|
|
||||||
continue
|
|
||||||
rev = r["review"]
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if r.get("provider_type") == "neurometric_prose":
|
lines.append(f"_reviewer: {MODEL_DISPLAY}_")
|
||||||
lines.append(f"**Verdict:** {rev['verdict']} _(heuristic, inferred from prose)_")
|
else:
|
||||||
lines.append("")
|
heading_verdict = review.get("verdict", "COMMENT")
|
||||||
# Cap at 6KB to keep PR comments readable; 4B model output is rarely over this
|
lines = [f"## CTO Review — {heading_verdict}", ""]
|
||||||
prose = rev.get("prose", "")
|
lines.append(f"**Verdict:** {heading_verdict}")
|
||||||
if len(prose) > 6000:
|
|
||||||
prose = prose[:6000] + "\n\n_…output truncated for comment length…_"
|
|
||||||
lines.append(prose)
|
|
||||||
lines.append("")
|
|
||||||
lines.append("_free tier; per-call token usage not tracked_")
|
|
||||||
lines.append("")
|
|
||||||
continue
|
|
||||||
lines.append(f"**Verdict:** {rev.get('verdict', 'COMMENT')}")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(rev.get("summary") or "_(model returned no summary)_")
|
lines.append(review.get("summary") or "_(model returned no summary)_")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if rev.get("concerns"):
|
if review.get("concerns"):
|
||||||
lines.append("**Concerns:**")
|
lines.append("**Concerns:**")
|
||||||
for c in rev["concerns"]:
|
for c in review["concerns"]:
|
||||||
sev = c["severity"].upper()
|
sev = c.get("severity", "UNKNOWN").upper()
|
||||||
f = f"`{c['file']}`" if c.get("file") else ""
|
f = f"`{c['file']}`" if c.get("file") else ""
|
||||||
lines.append(f"- **{sev}** {f} — {c['issue']}")
|
lines.append(f"- **{sev}** {f} — {c['issue']}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if rev.get("positives"):
|
if review.get("positives"):
|
||||||
lines.append("**Positives:** " + "; ".join(rev["positives"]))
|
lines.append("**Positives:** " + "; ".join(review["positives"]))
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"_tokens in/out: {r['prompt_tokens']}/{r['completion_tokens']} · cost: ${r['cost_cents']/100:.4f}_")
|
lines.append(f"_tokens in/out: {ptok}/{ctok} · cost: ${cost_cents/100:.4f}_")
|
||||||
total_cents += r["cost_cents"]
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append("---")
|
lines.append("---")
|
||||||
lines.append(f"_Total review cost: **${total_cents/100:.4f}**. A/B roster configured in `.github/workflows/cto-review.yml`._")
|
lines.append(f"_Total review cost: **${cost_cents/100:.4f}**. Reviewer: {MODEL_DISPLAY}. Roster configured in `.github/workflows/cto-review.yml`._")
|
||||||
|
|
||||||
with open("/tmp/review-body.md", "w") as f:
|
with open("/tmp/review-body.md", "w") as f:
|
||||||
f.write("\n".join(lines))
|
f.write("\n".join(lines))
|
||||||
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
|
||||||
f.write(f"added_cents={int(round(total_cents))}\n")
|
f.write(f"added_cents={int(round(cost_cents))}\n")
|
||||||
f.write(f"skipped=0\n")
|
f.write(f"skipped=0\n")
|
||||||
f.write(f"primary_verdict={heading_verdict}\n")
|
f.write(f"primary_verdict={heading_verdict}\n")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
- name: Post PR review (formal APPROVE / REQUEST_CHANGES / COMMENT)
|
- name: Post PR review (formal APPROVE / REQUEST_CHANGES / COMMENT)
|
||||||
# Uses primary model's verdict as the formal event. Falls back to COMMENT
|
# Uses Haiku's verdict as the formal event. Falls back to COMMENT
|
||||||
# if the primary model failed or returned something unexpected.
|
# if the model failed or returned something unexpected.
|
||||||
if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1'
|
if: steps.budget.outputs.skip != 'true' && steps.review.outputs.skipped != '1'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
env:
|
env:
|
||||||
|
|||||||
133
.github/workflows/deploy.yml
vendored
133
.github/workflows/deploy.yml
vendored
@@ -4,123 +4,22 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-peptide-starter-theme
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
# Gate deploy on the reusable CI (lint-php, phpcs, phpunit, file-size, lint-js).
|
||||||
name: Validate PHP & JS
|
# ci.yml exposes workflow_call so it can be used here without re-running a
|
||||||
runs-on: ubuntu-latest
|
# separate workflow — the call result satisfies the branch-protection context
|
||||||
|
# "ci / ci (lint-php (8.1))" etc.
|
||||||
steps:
|
ci:
|
||||||
- name: Checkout code
|
uses: ./.github/workflows/ci.yml
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: PHP Lint Check
|
|
||||||
run: |
|
|
||||||
find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20
|
|
||||||
test ${PIPESTATUS[0]} -eq 0
|
|
||||||
|
|
||||||
- name: Install PHPCS with WordPress standards
|
|
||||||
run: |
|
|
||||||
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
|
|
||||||
composer global require --dev squizlabs/php_codesniffer:"^3.0" wp-coding-standards/wpcs:"^3.0" phpcsstandards/phpcsutils:"^1.0"
|
|
||||||
|
|
||||||
- name: Run PHPCS (WordPress Core)
|
|
||||||
run: |
|
|
||||||
~/.composer/vendor/bin/phpcs -s .
|
|
||||||
|
|
||||||
- name: JS Syntax Check
|
|
||||||
run: |
|
|
||||||
find assets/js -name "*.js" -exec node --check {} \;
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: validate
|
name: Deploy (shared pipeline)
|
||||||
name: Deploy to Production
|
needs: ci
|
||||||
runs-on: ubuntu-latest
|
uses: peptide-repo/peptide-e2e/.github/workflows/deploy-app.yml@main
|
||||||
|
with:
|
||||||
steps:
|
target_path: wp-content/themes/peptide-starter
|
||||||
- name: Checkout code
|
secrets: inherit
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup SSH key
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
||||||
chmod 600 ~/.ssh/deploy_key
|
|
||||||
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/tmp/keyscan.err || {
|
|
||||||
echo "::warning::ssh-keyscan failed (non-fatal). stderr:"
|
|
||||||
cat /tmp/keyscan.err || true
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Verify deploy destination is a theme directory
|
|
||||||
run: |
|
|
||||||
# Safety check: abort if destination does not look like a WordPress theme.
|
|
||||||
# Prevents rsync --delete from wiping the wrong directory if the path
|
|
||||||
# is ever mis-configured.
|
|
||||||
DEST="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter"
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "test -f \"${DEST}/style.css\" || { echo \"ERROR: ${DEST}/style.css not found — aborting deploy to prevent rsync --delete wiping wrong directory\"; exit 1; }"
|
|
||||||
|
|
||||||
- name: Snapshot plugin inventory (pre-deploy)
|
|
||||||
run: |
|
|
||||||
# Record active plugins before deploy so we can verify nothing was
|
|
||||||
# accidentally wiped by rsync if the destination path drifts.
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null | tee /tmp/plugins_pre_deploy.txt && echo \"Pre-deploy plugin count: $(wc -l < /tmp/plugins_pre_deploy.txt)\""
|
|
||||||
|
|
||||||
- name: Deploy via rsync
|
|
||||||
run: |
|
|
||||||
rsync -avz --delete \
|
|
||||||
--exclude='.git' \
|
|
||||||
--exclude='.github' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='.phpcs.xml.dist' \
|
|
||||||
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no" \
|
|
||||||
./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/
|
|
||||||
|
|
||||||
- name: Verify plugin inventory unchanged (post-deploy)
|
|
||||||
run: |
|
|
||||||
# Fail the deploy if rsync wiped any plugins. Compares active plugin
|
|
||||||
# list before and after — any drop means something went wrong.
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
|
|
||||||
wp --path=/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html plugin list --status=active --fields=name --allow-root 2>/dev/null > /tmp/plugins_post_deploy.txt
|
|
||||||
PRE=$(wc -l < /tmp/plugins_pre_deploy.txt)
|
|
||||||
POST=$(wc -l < /tmp/plugins_post_deploy.txt)
|
|
||||||
echo "Pre-deploy active plugins: $PRE"
|
|
||||||
echo "Post-deploy active plugins: $POST"
|
|
||||||
if [ "$POST" -lt "$PRE" ]; then
|
|
||||||
echo "ERROR: Plugin count dropped from $PRE to $POST after deploy."
|
|
||||||
echo "Missing plugins:"
|
|
||||||
diff /tmp/plugins_pre_deploy.txt /tmp/plugins_post_deploy.txt || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Plugin inventory OK."
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
- name: Purge LiteSpeed Cache
|
|
||||||
run: |
|
|
||||||
ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \
|
|
||||||
${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH'
|
|
||||||
CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed"
|
|
||||||
if [ -d "$CACHE_DIR" ]; then
|
|
||||||
rm -rf "$CACHE_DIR"/*
|
|
||||||
echo "LiteSpeed cache purged."
|
|
||||||
fi
|
|
||||||
touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true
|
|
||||||
echo "Cache purge signal sent."
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
- name: Cleanup
|
|
||||||
if: always()
|
|
||||||
run: rm -f ~/.ssh/deploy_key
|
|
||||||
|
|
||||||
- name: Health check
|
|
||||||
run: |
|
|
||||||
sleep 5
|
|
||||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com)
|
|
||||||
if [ "$STATUS" != "200" ] && [ "$STATUS" != "403" ]; then
|
|
||||||
echo "ERROR: Site returned unexpected HTTP $STATUS after deploy"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Site is responsive (HTTP $STATUS)"
|
|
||||||
|
|||||||
22
.github/workflows/main-push-audit.yml
vendored
22
.github/workflows/main-push-audit.yml
vendored
@@ -29,12 +29,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const sha = context.sha;
|
const sha = context.sha;
|
||||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
// Squash-merge -> PR association is eventually-consistent; poll briefly
|
||||||
owner: context.repo.owner,
|
// before concluding this was a direct push (avoids false positives on merges).
|
||||||
repo: context.repo.repo,
|
async function findMergedPR() {
|
||||||
commit_sha: sha,
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
||||||
});
|
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||||
const merged = prs.find(p => p.merged_at);
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
commit_sha: sha,
|
||||||
|
});
|
||||||
|
const m = prs.find(p => p.merged_at);
|
||||||
|
if (m) return m;
|
||||||
|
if (attempt < 5) await new Promise(r => setTimeout(r, 10000));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const merged = await findMergedPR();
|
||||||
if (merged) {
|
if (merged) {
|
||||||
core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`);
|
core.info(`OK: commit ${sha.substring(0,7)} came from PR #${merged.number}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
276
CHANGELOG.md
276
CHANGELOG.md
@@ -1,177 +1,167 @@
|
|||||||
# Peptide Starter Theme - Changelog
|
# Peptide Starter Theme - Changelog
|
||||||
|
|
||||||
## [2.0.0-alpha.3] - 2026-04-26 — CMO walkthrough P1 fixes
|
## [2.3.0] - 2026-06-16 — Adopt estate CI standard
|
||||||
|
|
||||||
### Fixed
|
### Changed
|
||||||
- `template-parts/verdict/card.php`: Verdict text was a hardcoded generic placeholder.
|
|
||||||
Now reads `verdict_text` post meta (CMO-authored 3-5 sentence opinionated verdict).
|
- `.github/workflows/ci.yml`: Replaced per-repo CI jobs with a thin caller
|
||||||
Falls back to placeholder only if the field is empty (defensive, not normative).
|
delegating to `peptiderepo/peptide-e2e/.github/workflows/ci.yml@main`
|
||||||
- `template-parts/verdict/card.php`: Evidence-row glyphs were hardcoded by row position
|
(`tests: wp-framework`, `has_js: true`, `line_limit_paths: inc`). Added
|
||||||
(row 1=✓, row 2=!, row 3=?), producing wrong glyph/content pairings on Established and
|
`workflow_call` trigger so `deploy.yml` can gate on this workflow.
|
||||||
Cautionary verdicts. Now reads `signal_row_N_glyph` meta per row; renders no glyph if
|
- `.github/workflows/deploy.yml`: Retired standalone `validate` job; deploy
|
||||||
the field is empty rather than defaulting to a wrong one.
|
now gates on the reusable CI via `uses: ./.github/workflows/ci.yml`.
|
||||||
- `single-peptide.php`: Inline affiliate disclosure rendered on monographs with no partner
|
- `composer.json`: `test` script now calls `vendor/bin/phpunit` explicitly
|
||||||
link, directly contradicting the content ("We have no fulfillment partner at this time").
|
(was bare `phpunit`).
|
||||||
Now conditional on `has_partner_link` boolean meta (default false). Disclosure suppressed
|
- `inc/helpers.php`: Split 311-line file at the nav-walker boundary; reduced
|
||||||
unless a partner link is explicitly flagged by the CMO.
|
to 211 lines.
|
||||||
- `header.php`: Dark-mode toggle was live in the header. Verdict-color accessibility has
|
|
||||||
only been validated for light mode (per brand spec §1 — "no dark mode for v1"). Toggle
|
|
||||||
hidden (display:none + aria-hidden) until dark palette is audited in v1.1.
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `inc/verdict-meta.php`: New meta fields on `peptide` CPT:
|
|
||||||
- `verdict_text` (textarea, show_in_rest: true) — opinionated verdict paragraph
|
- `inc/nav-walker.php`: `Peptide_Starter_Nav_Walker` class and
|
||||||
- `signal_row_1/2/3_glyph` (enum ✓|!|?|⊘|⚠, show_in_rest: true) — per-row glyph
|
`peptide_starter_primary_menu_fallback()` extracted from `inc/helpers.php`
|
||||||
- `has_partner_link` (boolean, default false, show_in_rest: true) — disclosure gate
|
(123 lines). Loaded in `functions.php` before `helpers.php`.
|
||||||
- Meta box updated with textarea for verdict_text, glyph dropdowns per row, partner-link
|
- `package.json` / `package-lock.json`: Minimal npm manifest so the reusable
|
||||||
checkbox.
|
`lint-js` job can run `npm ci` and then execute `npm run lint` (which runs
|
||||||
- All 3 loaded monographs updated with correct `verdict_text`, `signal_row_N_glyph`, and
|
`node --check` on `assets/js/**/*.js`).
|
||||||
`has_partner_link=false`.
|
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0-alpha.2] - 2026-04-26 — Wire verdict card and disclosure into single-peptide.php
|
## [2.2.2] - 2026-04-29 — Front page layout restructure
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `template-parts/module-cards.php`: Removed Science Feed from the module card
|
||||||
|
grid. The grid is for interactive tools only; news is content. Grid is now
|
||||||
|
5 cards: Peptides, Calculator, Protocol Builder, Tracker, Subject Log. No card
|
||||||
|
in this set goes inactive-dimmed due to a plugin registration check.
|
||||||
|
- `front-page.php`: Replaced the full `[peptide_news]` inline feed with a
|
||||||
|
3-item teaser row (`[peptide_news count="3"]`) and a "View all research →"
|
||||||
|
CTA linking to `/news`. Reduces page weight and drives traffic to the
|
||||||
|
dedicated news page.
|
||||||
|
|
||||||
|
## [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
|
### Fixed
|
||||||
- `single-peptide.php`: Verdict card component was created in alpha.1 but never called
|
|
||||||
from the template — no verdict state was rendering on any peptide page. Now called
|
- `PEPTIDE_STARTER_VERSION` constant in `functions.php` was stale at
|
||||||
immediately after the page header for all `pr_peptide` posts with a `verdict_state`.
|
`2.0.0-alpha.3`; realigned to `2.2.1` to match `style.css` Version header.
|
||||||
- `single-peptide.php`: Affiliate disclosure (inline context) was created in alpha.1 but
|
|
||||||
never called. Now rendered directly after the content box (below `the_content()`),
|
## [2.1.8] - 2026-04-28 — Homepage verdict section spacing fix
|
||||||
ensuring it appears below any partner links in the post body per the editorial
|
|
||||||
disclosure spec.
|
### Fixed
|
||||||
- `single-peptide.php`: Duplicate `<h1>` issue — the page header was rendering an H1
|
|
||||||
title AND the verdict card was also rendering an H1. Fixed: page header H1 is now
|
- Added missing CSS for `hero-featured-verdicts`, `hero-verdict-card`, and
|
||||||
suppressed when a `verdict_state` is set; the verdict card owns the H1 for monograph
|
`hero-verdict-explainer` BEM classes used in `front-page.php`. These classes
|
||||||
posts.
|
were referenced in the template but had zero CSS rules, causing the Featured
|
||||||
|
Verdicts section to render with bare browser defaults (no grid layout, no card
|
||||||
|
padding, no visual separation between verdict entries).
|
||||||
|
- Added responsive 3-column grid for verdict cards at ≥900px breakpoint.
|
||||||
|
- Added 5-column grid for verdict explainer items at ≥900px, 3-column at ≥640px.
|
||||||
|
- Verdict card left-border now tints to match verdict state via CSS `:has()`.
|
||||||
|
|
||||||
|
|
||||||
All notable changes to the Peptide Starter Theme are documented in this file.
|
All notable changes to the Peptide Starter Theme are documented in this file.
|
||||||
|
|
||||||
## [2.0.0-alpha.1] - 2026-04-26 — Brand Identity v1.0.0 (Direction C: Trusted Guide)
|
## [2.1.7] - 2026-04-27 — Newsletter signup infrastructure (Beehiiv)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Brand identity CSS token system in `assets/css/brand.css` (Direction C palette: Teal/Lime/Orange)
|
|
||||||
- Poppins + Inter + IBM Plex Mono font loading (Google Fonts) with preload/preconnect
|
|
||||||
- Full favicon/PWA/OG asset pack from peptide-repo-brand v1.0.0:
|
|
||||||
- Logo variants: horizontal, horizontal-reverse, horizontal-on-teal, mark, mark-small, wordmark, wordmark-reverse, mono
|
|
||||||
- Favicons: ICO + PNG 16/32/48, apple-touch-icon
|
|
||||||
- PWA icons and manifest
|
|
||||||
- og-default.png (1200×630) for social sharing fallback
|
|
||||||
- `inc/verdict-meta.php` — Post meta registration for pr_peptide CPT:
|
|
||||||
- `verdict_state` (required): established|promising|investigational|insufficient|cautionary
|
|
||||||
- `signal_row_1/2/3` (optional): evidence signal labels
|
|
||||||
- Meta box in pr_peptide edit screen with sanitization and nonce protection
|
|
||||||
- Verdict badge component (`template-parts/verdict/badge.php`) with state-specific glyphs
|
|
||||||
- Verdict card component (`template-parts/verdict/card.php`) for monograph hero with evidence rows
|
|
||||||
- Evidence signal row sub-component (`template-parts/verdict/evidence-row.php`)
|
|
||||||
- Affiliate disclosure component (`template-parts/affiliate-disclosure.php`) with 3 contexts:
|
|
||||||
- Inline (adjacent to links)
|
|
||||||
- Banner (page-top disclosure)
|
|
||||||
- Footer (persistent sitewide)
|
|
||||||
- `page-how-we-review.php` — How We Review Peptides page template with ToC sidebar
|
|
||||||
- `page-about.php` — About & Editorial Standards page template with disclosure block
|
|
||||||
- Footer "Our Method" link section pointing to verdict explainer and editorial standards
|
|
||||||
- Footer affiliate disclosure via template-part (footer context)
|
|
||||||
- OG image fallback meta tag in header.php (og-default.png)
|
|
||||||
- Favicon and PWA manifest links in header.php with theme-color
|
|
||||||
- SVG logo support in header via inline `logo-horizontal.svg` (fallback to text if unavailable)
|
|
||||||
- Skip-to-content link in header.php (screen reader text)
|
|
||||||
- Reduced-motion media query in brand.css for button transitions
|
|
||||||
|
|
||||||
### Changed
|
- `page-subscribe.php` — single-column conversion template for `/subscribe/` landing page.
|
||||||
- `header.php`: Added font preconnect/preload/stylesheet links (Poppins, Inter, IBM Plex Mono)
|
No sticky ToC; signup-form-centric layout.
|
||||||
- `header.php`: Swapped logo function to use inline SVG `logo-horizontal.svg` if available
|
- `single-peptide.php` — compact Beehiiv signup block inserted after monograph article,
|
||||||
- `functions.php`: Updated version to 2.0.0-alpha.1
|
before prev/next navigation. Copy: "Want updates on monographs like this? One email a
|
||||||
- `functions.php`: Added `require_once` for new `inc/verdict-meta.php`
|
week, no spam." Form ID: `6edcd482-cbea-4767-8777-97ac17d6d609`.
|
||||||
- `functions.php`: Enqueued `assets/css/brand.css` before main stylesheet (dependency order)
|
- `footer.php` column 1 — "Get our weekly digest →" link to `/subscribe/` below brand blurb.
|
||||||
- `functions.php`: Footer widget area registration extended from 4 to 5 columns
|
- `functions.php` — `peptide_starter_enqueue_beehiiv_attribution()` loads Beehiiv's
|
||||||
- `footer.php`: Added "Our Method" footer section with two links
|
`attribution.js` site-wide (footer, async) for UTM source capture across all placements.
|
||||||
- `footer.php`: Integrated affiliate disclosure component (footer context)
|
|
||||||
- `inc/helpers.php`: Updated `peptide_starter_the_custom_logo()` to prefer SVG logo from `assets/brand/logo-horizontal.svg`
|
|
||||||
|
|
||||||
### Styling
|
|
||||||
- Brand CSS tokens cascade into existing theme; all new components use semantic token variables
|
|
||||||
- Verdict states (5-state taxonomy) signaled by color AND glyph AND label (never color-only)
|
|
||||||
- Lime (`#7FD600`) restricted to badge/button backgrounds with ink foreground (WCAG AA compliant)
|
|
||||||
- Prefers-reduced-motion honored for chip hover transitions
|
|
||||||
- All component BEM classes use `.pr-` prefix (brand) or `.ps-` prefix (theme); no collision with plugin prefixes (`.pn-`, `.psa-`, `.prab-`)
|
|
||||||
|
|
||||||
### Accessibility
|
|
||||||
- Verdict badge component: `role="img"` + `aria-label="Verdict: {State}"` for screen readers
|
|
||||||
- Glyph marked `aria-hidden="true"` to avoid redundant announcement
|
|
||||||
- Affiliate disclosure component: `<aside aria-label="Affiliate disclosure">` for context
|
|
||||||
- Skip-to-content link: `.screen-reader-text` class, keyboard-focusable
|
|
||||||
- All form inputs in verdict meta box properly labeled
|
|
||||||
- Evidence rows and card CTAs properly labeled and semantic
|
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- Requires brand assets to be copied into `assets/brand/`. All 9 SVG logos + 7 raster files included.
|
|
||||||
- Theme logo currently from hardcoded SVG; custom logo upload still supported via fallback.
|
|
||||||
- Verdict system is opt-in: card renders only if `verdict_state` meta is set; graceful fallback if not.
|
|
||||||
- Affiliate disclosure copy is hardcoded (not DB-driven) in v1. Can migrate to post meta or settings in v2.
|
|
||||||
- Footer extends to 5 columns; "Our Method" section can be replaced via widget for footer-5.
|
|
||||||
- Dark mode: verdict colors were validated for light mode only. Dark mode support planned for v1.1.
|
|
||||||
|
|
||||||
## [1.7.0] - 2026-04-24 — Remove WooCommerce integration
|
- Beehiiv form ID and embed script URL are compile-time constants in the templates.
|
||||||
|
If the form ID changes, update `single-peptide.php` and `page-subscribe.php`.
|
||||||
|
- `single-repo_daily.php` and `archive-repo_daily.php` signup placements deferred to
|
||||||
|
the Repo Daily sprint (templates don't exist yet).
|
||||||
|
- Sender: `repo-weekly@mail.peptiderepo.com`. DNS records (SPF/DKIM/DMARC for
|
||||||
|
`mail.peptiderepo.com`) to be added by Terence in Cloudflare when Beehiiv
|
||||||
|
sending-domain verification runs.
|
||||||
|
|
||||||
## [1.7.0] - 2026-04-24 — Remove WooCommerce integration
|
## [2.1.6] - 2026-04-27 — Our Methodology page template + footer link
|
||||||
|
|
||||||
WooCommerce is not used on peptiderepo.com. Removing dead code reduces the asset
|
### Added
|
||||||
dequeue surface and eliminates the function_exists() guard introduced in the 2026-04-23
|
|
||||||
hotfix.
|
|
||||||
|
|
||||||
### Removed
|
- `page-our-methodology.php` — two-column ToC template for the `/our-methodology/`
|
||||||
- WooCommerce asset dequeue block from `inc/perf-asset-policy.php` (styles:
|
editorial-standards page. Mirrors `page-how-we-review.php` layout; CSS classes
|
||||||
woocommerce-layout, woocommerce-smallscreen, woocommerce-general; scripts:
|
use `page-our-methodology` prefix. The `#last-verified` anchor on this page is the
|
||||||
wc-add-to-cart, woocommerce, sourcebuster-js, wc-order-attribution)
|
deep-link target for Phase 2 verdict-card "Last verified" lines.
|
||||||
- Cart icon in `header.php` (was conditional on `class_exists('WooCommerce')`)
|
- Footer "Our Method" section now includes **Our Methodology** link (`/our-methodology/`)
|
||||||
- `peptide_starter_perf_woocommerce_styles` and `peptide_starter_perf_woocommerce_scripts`
|
alongside How We Review Peptides and Editorial Standards.
|
||||||
filter hooks
|
|
||||||
- WooCommerce-specific tests from `tests/test-perf-asset-policy.php`
|
### Notes
|
||||||
- WooCommerce section from README.md and CONVENTIONS.md
|
|
||||||
|
- No changes to existing templates, CSS, or JS.
|
||||||
|
- `footer.php` also backports the section-5 "Our Method" block from the v2.1.5 brand
|
||||||
|
sprint which was live on the server but not committed to the repo.
|
||||||
|
|
||||||
## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1
|
## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1
|
||||||
|
|
||||||
Mobile-perf optimization targeting LCP reduction through conditional asset
|
Mobile optimization targeting LCP reduction through conditional asset dequeue,
|
||||||
dequeue, font weight slimming, and font server preconnect. Measured baseline
|
font weight slimming, and font server preconnect. Measured baseline LCP on
|
||||||
LCP on Moto G Power (Slow 4G): 5.4s; target after Phase 1: ~2.5s.
|
Moto G Power (Slow 4G): 5.4s; target: sub-4s after Phase 1 deployment.
|
||||||
|
|
||||||
### Added
|
### Performance improvements
|
||||||
|
|
||||||
- `inc/perf-asset-policy.php` — five-function module:
|
- **Conditional asset dequeue**: WooCommerce, Elementor, and USMI assets now
|
||||||
- `peptide_starter_perf_dequeue_plugin_assets()` — dequeues WooCommerce,
|
dequeue on pages that don't use them. Removes ~100KB from non-shop pages.
|
||||||
Elementor, and Ultimate Social Media Icons assets on pages that don't
|
- **Google Fonts slimming**: Roboto and Roboto Slab trimmed from 72 faces to 5
|
||||||
use them. Hook: `wp_enqueue_scripts` priority 100.
|
(400/500/700 weights, no italics). Font CSS shrinks ~6KB → <1KB; woff2 payload
|
||||||
- `peptide_starter_perf_slim_google_fonts()` — `style_loader_src` filter;
|
reduced ~80%.
|
||||||
rewrites Roboto and Roboto Slab `family=` queries to keep only weights
|
- **Preconnect hints**: `fonts.googleapis.com` and `fonts.gstatic.com` now
|
||||||
actually used (400/500/700 + 400/700). Drops 72 font faces to 5.
|
preconnect in `<head>`, eliminating separate TLS handshake latency on cold cache.
|
||||||
- `peptide_starter_perf_resource_hints()` — `wp_resource_hints` filter;
|
- **Cookie-notice defer**: Cookie banner script deferred to unblock initial render.
|
||||||
adds preconnect hints for `fonts.googleapis.com` and `fonts.gstatic.com`.
|
|
||||||
- `peptide_starter_perf_defer_cookie_notice()` — `script_loader_tag` filter;
|
|
||||||
appends `defer` attribute to the cookie-notice front-end script.
|
|
||||||
- `peptide_starter_page_uses_elementor( int $post_id )` — helper.
|
|
||||||
- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default `true`).
|
|
||||||
Define `false` in `wp-config.php` to disable all dequeues instantly.
|
|
||||||
- Filterable handle lists: `peptide_starter_perf_woocommerce_handles`,
|
|
||||||
`peptide_starter_perf_elementor_handles`, `peptide_starter_perf_usmi_handles`,
|
|
||||||
`peptide_starter_perf_font_weights`.
|
|
||||||
- `tests/test-perf-asset-policy.php` — 13 unit-test cases covering kill-switch,
|
|
||||||
per-page-context dequeue logic, font slim happy/edge cases, filter overrides,
|
|
||||||
preconnect insertion, defer attribute application.
|
|
||||||
|
|
||||||
### Changed
|
### Technical
|
||||||
|
|
||||||
- `style.css` Version header bumped 1.5.2 → 1.6.0.
|
- New module `inc/perf-asset-policy.php` (~245 lines, PHPUnit tested).
|
||||||
- `PEPTIDE_STARTER_VERSION` constant bumped to `1.6.0`.
|
- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default true) allows
|
||||||
|
disable via `wp-config.php` for rollback.
|
||||||
|
- All dequeue lists and font weights exposed as filters for customization.
|
||||||
|
- Production handle verification: WC (layout/smallscreen/general), Elementor
|
||||||
|
(frontend/frontend-legacy + per-post CSS), USMI (SFSImainCss + 4 scripts).
|
||||||
|
|
||||||
### Why
|
### Tests
|
||||||
|
|
||||||
PageSpeed Insights mobile score on `https://peptiderepo.com/` was 74 with
|
- `test-perf-asset-policy.php` — 8 new unit cases covering dequeue conditions
|
||||||
LCP 5.4s, FCP 2.6s. Desktop scored ~95 — the gap was mobile-specific
|
(shop/non-shop/Elementor/USMI), font rewrite happy path and edge cases,
|
||||||
(weaker CPU + slower network amplify the cost of 13 render-blocking CSS
|
preconnect addition, defer application, and kill-switch disable.
|
||||||
files in `<head>`, of which the homepage actually needed at most 4).
|
|
||||||
Phase 1 trims the asset graph for non-shop pages without touching plugin
|
### Out of scope
|
||||||
code or template files.
|
|
||||||
|
- Critical CSS inline (Phase 2 candidate after measuring Phase 1 lift).
|
||||||
|
- jQuery dequeue (WC/PSA/Elementor compat risk too high).
|
||||||
|
- Image optimizations (no images on homepage critical path).
|
||||||
|
- LiteSpeed Cache settings (CTO tunes post-deploy if Phase 1 insufficient).
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
None. All optimizations are transparent to plugin authors; dequeues use
|
||||||
|
standard WordPress APIs and can be overridden via filters or re-enqueued by
|
||||||
|
plugins if needed.
|
||||||
|
|
||||||
## [1.5.2] - 2026-04-14 — Security
|
## [1.5.2] - 2026-04-14 — Security
|
||||||
|
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} )();
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "phpcs",
|
"lint": "phpcs",
|
||||||
"lint:fix": "phpcbf",
|
"lint:fix": "phpcbf",
|
||||||
"test": "phpunit"
|
"test": "vendor/bin/phpunit"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
// Default footer content if no widgets.
|
// Default footer content if no widgets.
|
||||||
if ( 1 === $i ) {
|
if ( 1 === $i ) {
|
||||||
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
|
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<p>' . esc_html__( 'Peptide Repo is a comprehensive scientific database for peptide research.', 'peptide-starter' ) . '</p>';
|
echo '<p>' . esc_html__( 'Verdict-driven peptide guides for clinicians and informed readers. Clear about the evidence — including when there isn\'t any.', 'peptide-starter' ) . '</p>';
|
||||||
|
echo '<p style="margin-top: var(--spacing-md); font-size: 0.9rem;"><a href="' . esc_url( home_url( '/subscribe' ) ) . '" style="color: var(--color-accent-green, #5a7c5a); font-weight: 600;">' . esc_html__( 'Get our weekly digest →', 'peptide-starter' ) . '</a></p>';
|
||||||
} elseif ( 2 === $i ) {
|
} elseif ( 2 === $i ) {
|
||||||
echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>';
|
echo '<h3>' . esc_html__( 'Quick Links', 'peptide-starter' ) . '</h3>';
|
||||||
echo '<ul>';
|
echo '<ul>';
|
||||||
@@ -56,6 +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( '/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>';
|
||||||
}
|
}
|
||||||
|
|||||||
175
front-page.php
175
front-page.php
@@ -2,21 +2,38 @@
|
|||||||
/**
|
/**
|
||||||
* The front page template
|
* The front page template
|
||||||
*
|
*
|
||||||
* Layout: Hero → Research Modules → PRAutoBlogger posts → Peptide News feed.
|
* Layout: Hero → Verdict Explainer → Featured Verdict Cards → Research Modules
|
||||||
* Newsletter signup is handled by footer.php (via template part) on the front page.
|
* → PRAutoBlogger posts → Peptide News feed.
|
||||||
*
|
*
|
||||||
* @see template-parts/module-cards.php — research modules grid
|
* Brand v2.0.0: Direction C "The Trusted Guide". Hero uses the 5-state verdict
|
||||||
* @see template-parts/newsletter-signup.php — included via footer.php on front page
|
* taxonomy as the central explainer hook. Three featured verdict cards pull from
|
||||||
* @see functions.php — helper functions for hero content
|
* real monographs (semaglutide / BPC-157 / Melanotan-II) via post meta.
|
||||||
|
*
|
||||||
|
* @see template-parts/module-cards.php — research modules grid
|
||||||
|
* @see template-parts/verdict/badge.php — verdict badge component
|
||||||
|
* @see template-parts/verdict/card.php — full verdict card (monograph hero)
|
||||||
|
* @see inc/helpers.php — hero title / subtitle getters
|
||||||
|
* @see assets/css/brand.css — verdict token definitions
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get_header();
|
get_header();
|
||||||
|
|
||||||
|
/** @see inc/verdict-helpers.php — peptide_starter_get_verdict_taxonomy() */
|
||||||
|
$verdict_taxonomy = peptide_starter_get_verdict_taxonomy();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Featured monograph post IDs. These are the three exemplar monographs
|
||||||
|
* loaded during brand sprint v2.0.0 (2026-04-26).
|
||||||
|
* Semaglutide=211 (established), BPC-157=36 (investigational), Melanotan-II=177 (cautionary).
|
||||||
|
*/
|
||||||
|
$featured_monograph_ids = array( 211, 36, 177 );
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<main id="main" class="site-main">
|
<main id="main" class="site-main">
|
||||||
<!-- Hero Section -->
|
|
||||||
|
<!-- HERO SECTION -->
|
||||||
<section class="hero-section">
|
<section class="hero-section">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1 class="hero-title">
|
<h1 class="hero-title">
|
||||||
@@ -60,14 +77,134 @@ get_header();
|
|||||||
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary">
|
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary">
|
||||||
<?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?>
|
<?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?>
|
||||||
</a>
|
</a>
|
||||||
<a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>" class="ps-btn ps-btn-secondary">
|
<a href="<?php echo esc_url( home_url( '/how-we-review-peptides' ) ); ?>" class="ps-btn ps-btn-secondary">
|
||||||
<?php esc_html_e( 'Documentation', 'peptide-starter' ); ?>
|
<?php esc_html_e( 'Our method', 'peptide-starter' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Research Modules Grid (6 cards) -->
|
<!-- VERDICT TAXONOMY EXPLAINER -->
|
||||||
|
<section class="hero-verdict-explainer" aria-label="<?php esc_attr_e( 'Our verdict system', 'peptide-starter' ); ?>">
|
||||||
|
<div class="ps-container">
|
||||||
|
<p class="hero-verdict-explainer__heading">
|
||||||
|
<?php esc_html_e( 'Every peptide gets a verdict. Here\'s what they mean.', 'peptide-starter' ); ?>
|
||||||
|
</p>
|
||||||
|
<div class="hero-verdict-explainer__grid">
|
||||||
|
<?php foreach ( $verdict_taxonomy as $v ) : ?>
|
||||||
|
<div class="hero-verdict-explainer__item">
|
||||||
|
<span
|
||||||
|
class="pr-verdict pr-verdict--<?php echo esc_attr( $v['state'] ); ?>"
|
||||||
|
role="img"
|
||||||
|
aria-label="<?php echo esc_attr( sprintf( __( 'Verdict: %s', 'peptide-starter' ), $v['label'] ) ); ?>"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<p class="hero-verdict-explainer__cta-line">
|
||||||
|
<a href="<?php echo esc_url( home_url( '/how-we-review-peptides' ) ); ?>" class="ps-btn ps-btn-tertiary ps-btn-sm">
|
||||||
|
<?php esc_html_e( 'How we assign verdicts →', 'peptide-starter' ); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FEATURED VERDICT CARDS: semaglutide=211 (Established), BPC-157=36 (Investigational), Melanotan-II=177 (Cautionary) -->
|
||||||
|
<?php
|
||||||
|
$featured_posts = get_posts(
|
||||||
|
array(
|
||||||
|
'post__in' => $featured_monograph_ids,
|
||||||
|
'post_type' => 'peptide',
|
||||||
|
'posts_per_page' => 3,
|
||||||
|
'orderby' => 'post__in',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if ( ! empty( $featured_posts ) ) :
|
||||||
|
?>
|
||||||
|
<section class="hero-featured-verdicts" aria-label="<?php esc_attr_e( 'Featured peptide verdicts', 'peptide-starter' ); ?>">
|
||||||
|
<div class="ps-container">
|
||||||
|
<h2 class="hero-featured-verdicts__title"><?php esc_html_e( 'Featured verdicts', 'peptide-starter' ); ?></h2>
|
||||||
|
<p class="hero-featured-verdicts__subtitle">
|
||||||
|
<?php esc_html_e( 'Real assessments from our database — see the system in action.', 'peptide-starter' ); ?>
|
||||||
|
</p>
|
||||||
|
<div class="hero-featured-verdicts__grid">
|
||||||
|
<?php
|
||||||
|
foreach ( $featured_posts as $featured_post ) :
|
||||||
|
$post_id = $featured_post->ID;
|
||||||
|
$verdict_state = get_post_meta( $post_id, 'verdict_state', true );
|
||||||
|
$verdict_text = get_post_meta( $post_id, 'verdict_text', true );
|
||||||
|
$post_url = get_permalink( $post_id );
|
||||||
|
|
||||||
|
// Verdict state config — @see inc/verdict-helpers.php
|
||||||
|
$verdict_config = peptide_starter_get_verdict_config();
|
||||||
|
$vc = isset( $verdict_config[ $verdict_state ] ) ? $verdict_config[ $verdict_state ] : null;
|
||||||
|
|
||||||
|
// Signal rows (up to 3).
|
||||||
|
$signal_rows = array();
|
||||||
|
for ( $i = 1; $i <= 3; $i++ ) {
|
||||||
|
$row_label = get_post_meta( $post_id, 'signal_row_' . $i . '_label', true );
|
||||||
|
$row_glyph = get_post_meta( $post_id, 'signal_row_' . $i . '_glyph', true );
|
||||||
|
if ( $row_label ) {
|
||||||
|
$signal_rows[] = array(
|
||||||
|
'label' => $row_label,
|
||||||
|
'glyph' => $row_glyph ? $row_glyph : '•',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<article class="hero-verdict-card" aria-label="<?php echo esc_attr( get_the_title( $post_id ) ); ?>">
|
||||||
|
<?php if ( $vc ) : ?>
|
||||||
|
<span
|
||||||
|
class="pr-verdict pr-verdict--<?php echo esc_attr( $verdict_state ); ?> hero-verdict-card__badge"
|
||||||
|
role="img"
|
||||||
|
aria-label="<?php echo esc_attr( sprintf( __( 'Verdict: %s', 'peptide-starter' ), $vc['label'] ) ); ?>"
|
||||||
|
>
|
||||||
|
<span class="pr-verdict__glyph" aria-hidden="true"><?php echo esc_html( $vc['glyph'] ); ?></span>
|
||||||
|
<span class="pr-verdict__label"><?php echo esc_html( $vc['label'] ); ?></span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<h3 class="hero-verdict-card__title">
|
||||||
|
<a href="<?php echo esc_url( $post_url ); ?>"><?php echo esc_html( get_the_title( $post_id ) ); ?></a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<?php if ( $verdict_text ) : ?>
|
||||||
|
<p class="hero-verdict-card__summary"><?php echo esc_html( $verdict_text ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $signal_rows ) ) : ?>
|
||||||
|
<ul class="hero-verdict-card__signals" aria-label="<?php esc_attr_e( 'Evidence signals', 'peptide-starter' ); ?>">
|
||||||
|
<?php foreach ( $signal_rows as $row ) : ?>
|
||||||
|
<li class="hero-verdict-card__signal-item">
|
||||||
|
<span class="hero-verdict-card__signal-glyph" aria-hidden="true"><?php echo esc_html( $row['glyph'] ); ?></span>
|
||||||
|
<?php echo esc_html( $row['label'] ); ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo esc_url( $post_url ); ?>" class="hero-verdict-card__link">
|
||||||
|
<?php esc_html_e( 'Read the full evidence →', 'peptide-starter' ); ?>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-featured-verdicts__footer">
|
||||||
|
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-secondary">
|
||||||
|
<?php esc_html_e( 'Browse all peptides', 'peptide-starter' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Research Modules Grid (5 cards) -->
|
||||||
<?php get_template_part( 'template-parts/module', 'cards' ); ?>
|
<?php get_template_part( 'template-parts/module', 'cards' ); ?>
|
||||||
|
|
||||||
<!-- PRAutoBlogger Posts Widget -->
|
<!-- PRAutoBlogger Posts Widget -->
|
||||||
@@ -77,24 +214,28 @@ get_header();
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- News Feed Section -->
|
<!-- News Teaser Section — 3 items + "View all" CTA -->
|
||||||
<?php
|
<?php
|
||||||
if ( shortcode_exists( 'peptide_news' ) ) {
|
if ( shortcode_exists( 'peptide_news' ) ) {
|
||||||
?>
|
?>
|
||||||
<section class="news-feed-section">
|
<section class="news-teaser-section">
|
||||||
<div class="ps-container">
|
<div class="ps-container">
|
||||||
<div class="news-feed-header">
|
<div class="news-teaser-header">
|
||||||
<h2 class="news-feed-title"><?php esc_html_e( 'Latest Peptide Research', 'peptide-starter' ); ?></h2>
|
<div>
|
||||||
<p class="news-feed-description">
|
<h2><?php esc_html_e( 'Latest Peptide Research', 'peptide-starter' ); ?></h2>
|
||||||
<?php esc_html_e( 'Stay updated with the latest developments in peptide science and research.', 'peptide-starter' ); ?>
|
<p><?php esc_html_e( 'Recent publications from leading research sources.', 'peptide-starter' ); ?></p>
|
||||||
</p>
|
</div>
|
||||||
|
<a href="<?php echo esc_url( home_url( '/news' ) ); ?>" class="ps-btn ps-btn-secondary">
|
||||||
|
<?php esc_html_e( 'View all research →', 'peptide-starter' ); ?>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<?php echo do_shortcode( '[peptide_news]' ); ?>
|
<?php echo do_shortcode( '[peptide_news count="3"]' ); ?>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<?php get_footer(); ?>
|
<?php get_footer(); ?>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
* 300 lines and to make responsibilities discoverable.
|
* 300 lines and to make responsibilities discoverable.
|
||||||
*
|
*
|
||||||
* @see inc/config.php — security configuration
|
* @see inc/config.php — security configuration
|
||||||
|
* @see inc/nav-walker.php — Peptide_Starter_Nav_Walker + primary menu fallback
|
||||||
|
* @see inc/helpers.php — customizer helpers, gates, honeypot, CSV utility
|
||||||
* @see inc/rate-limiter.php — transient-backed rate limiter
|
* @see inc/rate-limiter.php — transient-backed rate limiter
|
||||||
* @see inc/email-verification.php — token-gated email verification
|
* @see inc/email-verification.php — token-gated email verification
|
||||||
* @see inc/auth-handlers.php — AJAX login + registration
|
* @see inc/auth-handlers.php — AJAX login + registration
|
||||||
@@ -14,7 +16,6 @@
|
|||||||
* @see inc/newsletter-admin.php — admin subscriber viewer + unsubscribe
|
* @see inc/newsletter-admin.php — admin subscriber viewer + unsubscribe
|
||||||
* @see inc/page-setup.php — auto-create pages + v1.5.0 user migration
|
* @see inc/page-setup.php — auto-create pages + v1.5.0 user migration
|
||||||
* @see inc/mail-diagnostic.php — admin deliverability test tool
|
* @see inc/mail-diagnostic.php — admin deliverability test tool
|
||||||
* @see inc/helpers.php — nav walker, menu fallback, gates, utilities
|
|
||||||
* @see inc/perf-asset-policy.php — asset dequeue + font slim + preconnect + defer
|
* @see inc/perf-asset-policy.php — asset dequeue + font slim + preconnect + defer
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
@@ -26,13 +27,14 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Theme constants.
|
// Theme constants.
|
||||||
define( 'PEPTIDE_STARTER_VERSION', '2.0.0-alpha.3' );
|
define( 'PEPTIDE_STARTER_VERSION', '2.3.0' );
|
||||||
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() );
|
||||||
|
|
||||||
// Load feature modules. Order matters — config must load before anything
|
// Load feature modules. Order matters — config must load before anything
|
||||||
// that calls peptide_starter_config_*; rate limiter before handlers.
|
// that calls peptide_starter_config_*; rate limiter before handlers.
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/config.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/config.php';
|
||||||
|
require_once PEPTIDE_STARTER_DIR . '/inc/nav-walker.php';
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/helpers.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/helpers.php';
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/cloudflare-ips.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/cloudflare-ips.php';
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/rate-limiter.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/rate-limiter.php';
|
||||||
@@ -45,6 +47,7 @@ require_once PEPTIDE_STARTER_DIR . '/inc/newsletter-admin.php';
|
|||||||
require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php';
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php';
|
||||||
require_once PEPTIDE_STARTER_DIR . '/inc/verdict-meta.php';
|
require_once PEPTIDE_STARTER_DIR . '/inc/verdict-meta.php';
|
||||||
|
require_once PEPTIDE_STARTER_DIR . '/inc/verdict-helpers.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up theme defaults and register support for WordPress features.
|
* Set up theme defaults and register support for WordPress features.
|
||||||
@@ -87,6 +90,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',
|
||||||
@@ -243,3 +251,20 @@ function peptide_starter_inline_dark_mode_script() {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
|
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue Beehiiv attribution script site-wide.
|
||||||
|
*
|
||||||
|
* Loads Beehiiv's attribution.js in the footer so all signup form placements
|
||||||
|
* (subscribe page iframe, single-peptide inline form) are tracked correctly
|
||||||
|
* under a single script include.
|
||||||
|
*
|
||||||
|
* Who calls it: wp_enqueue_scripts action (all front-end page loads).
|
||||||
|
* Dependencies: none.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function peptide_starter_enqueue_beehiiv_attribution() {
|
||||||
|
wp_enqueue_script( 'beehiiv-attribution', 'https://subscribe-forms.beehiiv.com/attribution.js', array(), null, true );
|
||||||
|
}
|
||||||
|
add_action( 'wp_enqueue_scripts', 'peptide_starter_enqueue_beehiiv_attribution' );
|
||||||
|
|||||||
132
inc/helpers.php
132
inc/helpers.php
@@ -2,17 +2,21 @@
|
|||||||
/**
|
/**
|
||||||
* Theme Helper Functions
|
* Theme Helper Functions
|
||||||
*
|
*
|
||||||
* Customizer output helpers, nav walker, custom logo, pagination, menu
|
* Customizer output helpers, custom logo, pagination, auth gates,
|
||||||
* fallback, auth gates, and assorted utilities shared across the theme.
|
* honeypot, and CSV utility shared across the theme.
|
||||||
*
|
*
|
||||||
* @see functions.php — includes this file
|
* Navigation walker and primary menu fallback have been extracted to
|
||||||
* @see header.php — uses peptide_starter_the_custom_logo() + menu fallback
|
* inc/nav-walker.php (v2.3.0) to keep this file under 300 lines.
|
||||||
|
*
|
||||||
|
* @see functions.php — includes this file (after nav-walker.php)
|
||||||
|
* @see inc/nav-walker.php — Peptide_Starter_Nav_Walker + menu fallback
|
||||||
|
* @see header.php — uses peptide_starter_the_custom_logo()
|
||||||
* @see page-subject-log.php, page-tracker.php, page-protocol-builder.php —
|
* @see page-subject-log.php, page-tracker.php, page-protocol-builder.php —
|
||||||
* use peptide_starter_require_login()
|
* use peptide_starter_require_login()
|
||||||
*
|
*
|
||||||
* What: Utility functions for template output, theme mods, auth gating.
|
* What: Utility functions for template output, theme mods, auth gating.
|
||||||
* Who calls it: Templates and handler files.
|
* Who calls it: Templates and handler files.
|
||||||
* Dependencies: WordPress Customizer API, Walker_Nav_Menu, email-verification.
|
* Dependencies: WordPress Customizer API, email-verification.
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
*/
|
*/
|
||||||
@@ -22,110 +26,6 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigation Menu Walker — adds active classes.
|
|
||||||
*
|
|
||||||
* What: Renders menu items with an 'active' class for current + ancestor.
|
|
||||||
* Who calls it: wp_nav_menu() in header.php with walker => new instance.
|
|
||||||
* Dependencies: Walker_Nav_Menu.
|
|
||||||
*/
|
|
||||||
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a single menu item.
|
|
||||||
*
|
|
||||||
* @param string $output Item output accumulator.
|
|
||||||
* @param WP_Post $item Menu item data.
|
|
||||||
* @param int $depth Menu depth.
|
|
||||||
* @param stdClass $args Menu arguments.
|
|
||||||
* @param int $id Current item ID.
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
|
|
||||||
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
|
|
||||||
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
|
|
||||||
$classes[] = 'menu-item-' . $item->ID;
|
|
||||||
|
|
||||||
if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) {
|
|
||||||
$classes[] = 'active';
|
|
||||||
}
|
|
||||||
|
|
||||||
$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
|
|
||||||
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
|
|
||||||
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
|
|
||||||
|
|
||||||
$item_id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
|
|
||||||
$item_id = $item_id ? ' id="' . esc_attr( $item_id ) . '"' : '';
|
|
||||||
|
|
||||||
$output .= $indent . '<li' . $item_id . $class_names . '>';
|
|
||||||
|
|
||||||
$atts = array(
|
|
||||||
'title' => ! empty( $item->attr_title ) ? $item->attr_title : '',
|
|
||||||
'target' => ! empty( $item->target ) ? $item->target : '',
|
|
||||||
'rel' => ! empty( $item->xfn ) ? $item->xfn : '',
|
|
||||||
'href' => ! empty( $item->url ) ? $item->url : '',
|
|
||||||
);
|
|
||||||
|
|
||||||
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
|
|
||||||
$attributes = '';
|
|
||||||
|
|
||||||
foreach ( $atts as $attr => $value ) {
|
|
||||||
if ( ! empty( $value ) ) {
|
|
||||||
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
|
|
||||||
$attributes .= ' ' . $attr . '="' . $value . '"';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = apply_filters( 'the_title', $item->title, $item->ID );
|
|
||||||
|
|
||||||
$link = $args->before;
|
|
||||||
$link .= '<a' . $attributes . '>';
|
|
||||||
$link .= $args->link_before . $title . $args->link_after;
|
|
||||||
$link .= '</a>';
|
|
||||||
$link .= $args->after;
|
|
||||||
|
|
||||||
$output .= $link . "</li>\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback menu when no primary nav is assigned in WP admin.
|
|
||||||
*
|
|
||||||
* Previously lived inline inside header.php; moved here in v1.5.1 to
|
|
||||||
* prevent redeclaration if header.php is ever included twice.
|
|
||||||
*
|
|
||||||
* @return void Echoes a complete <ul>.
|
|
||||||
*/
|
|
||||||
function peptide_starter_primary_menu_fallback() {
|
|
||||||
?>
|
|
||||||
<ul>
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Home', 'peptide-starter' ); ?></a></li>
|
|
||||||
<li class="menu-item-has-children">
|
|
||||||
<a href="#"><?php esc_html_e( 'Tools', 'peptide-starter' ); ?></a>
|
|
||||||
<ul class="sub-menu">
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/calculator' ) ); ?>"><?php esc_html_e( 'Calculator', 'peptide-starter' ); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/protocol-builder' ) ); ?>"><?php esc_html_e( 'Protocol Builder', 'peptide-starter' ); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/tracker' ) ); ?>"><?php esc_html_e( 'Tracker', 'peptide-starter' ); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="menu-item-has-children">
|
|
||||||
<a href="#"><?php esc_html_e( 'My Data', 'peptide-starter' ); ?></a>
|
|
||||||
<ul class="sub-menu">
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>"><?php esc_html_e( 'Peptides', 'peptide-starter' ); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/subject-log' ) ); ?>"><?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="menu-item-has-children">
|
|
||||||
<a href="#"><?php esc_html_e( 'Resources', 'peptide-starter' ); ?></a>
|
|
||||||
<ul class="sub-menu">
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>"><?php esc_html_e( 'Documentation', 'peptide-starter' ); ?></a></li>
|
|
||||||
<li><a href="<?php echo esc_url( home_url( '/news' ) ); ?>"><?php esc_html_e( 'Science Feed', 'peptide-starter' ); ?></a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display custom logo with fallback to site name link.
|
* Display custom logo with fallback to site name link.
|
||||||
*
|
*
|
||||||
@@ -155,7 +55,7 @@ function peptide_starter_the_custom_logo() {
|
|||||||
* @return string Hero title text.
|
* @return string Hero title text.
|
||||||
*/
|
*/
|
||||||
function peptide_starter_get_hero_title() {
|
function peptide_starter_get_hero_title() {
|
||||||
return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
|
return get_theme_mod( 'hero_title', esc_html__( 'Is this peptide right for you?', 'peptide-starter' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +64,7 @@ function peptide_starter_get_hero_title() {
|
|||||||
* @return string Hero subtitle text.
|
* @return string Hero subtitle text.
|
||||||
*/
|
*/
|
||||||
function peptide_starter_get_hero_subtitle() {
|
function peptide_starter_get_hero_subtitle() {
|
||||||
return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
|
return get_theme_mod( 'hero_subtitle', esc_html__( 'What the research really says', 'peptide-starter' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,7 +82,7 @@ function peptide_starter_get_search_placeholder() {
|
|||||||
* @return string Copyright text (may contain safe HTML).
|
* @return string Copyright text (may contain safe HTML).
|
||||||
*/
|
*/
|
||||||
function peptide_starter_get_footer_copyright() {
|
function peptide_starter_get_footer_copyright() {
|
||||||
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
|
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright \u00a9 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,8 +117,8 @@ function peptide_starter_pagination() {
|
|||||||
'format' => 'page/%#%/',
|
'format' => 'page/%#%/',
|
||||||
'current' => $current,
|
'current' => $current,
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'prev_text' => esc_html__( '← Previous', 'peptide-starter' ),
|
'prev_text' => esc_html__( '\u2190 Previous', 'peptide-starter' ),
|
||||||
'next_text' => esc_html__( 'Next →', 'peptide-starter' ),
|
'next_text' => esc_html__( 'Next \u2192', 'peptide-starter' ),
|
||||||
'type' => 'plain',
|
'type' => 'plain',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -248,8 +148,8 @@ function peptide_starter_safe_referer() {
|
|||||||
/**
|
/**
|
||||||
* Gate a template behind login + verified email.
|
* Gate a template behind login + verified email.
|
||||||
*
|
*
|
||||||
* Unauthenticated → redirect to /auth?redirect_to=current-uri.
|
* Unauthenticated \u2192 redirect to /auth?redirect_to=current-uri.
|
||||||
* Logged in but unverified → redirect to /profile?verify_required=1.
|
* Logged in but unverified \u2192 redirect to /profile?verify_required=1.
|
||||||
*
|
*
|
||||||
* Side effects: emits 302 and exits the request.
|
* Side effects: emits 302 and exits the request.
|
||||||
*
|
*
|
||||||
|
|||||||
123
inc/nav-walker.php
Normal file
123
inc/nav-walker.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Navigation Menu Walker & Primary Menu Fallback
|
||||||
|
*
|
||||||
|
* What: Custom nav walker that adds 'active' CSS classes, plus a fallback
|
||||||
|
* menu rendered when no primary nav is assigned in WP admin.
|
||||||
|
* Who calls it: header.php — wp_nav_menu() with walker => new Peptide_Starter_Nav_Walker(),
|
||||||
|
* and the callback_function parameter for the fallback.
|
||||||
|
* Dependencies: Walker_Nav_Menu (WordPress core).
|
||||||
|
*
|
||||||
|
* Extracted from inc/helpers.php in v2.3.0 to keep both files under 300 lines.
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly.
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation Menu Walker — adds active classes.
|
||||||
|
*
|
||||||
|
* What: Renders menu items with an 'active' class for current + ancestor items.
|
||||||
|
* Who calls it: wp_nav_menu() in header.php with walker => new instance.
|
||||||
|
* Dependencies: Walker_Nav_Menu.
|
||||||
|
*/
|
||||||
|
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a single menu item.
|
||||||
|
*
|
||||||
|
* @param string $output Item output accumulator (passed by reference).
|
||||||
|
* @param WP_Post $item Menu item data.
|
||||||
|
* @param int $depth Menu depth.
|
||||||
|
* @param stdClass $args Menu arguments.
|
||||||
|
* @param int $id Current item ID.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
|
||||||
|
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
|
||||||
|
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
|
||||||
|
$classes[] = 'menu-item-' . $item->ID;
|
||||||
|
|
||||||
|
if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) {
|
||||||
|
$classes[] = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
|
||||||
|
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
|
||||||
|
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
|
||||||
|
|
||||||
|
$item_id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
|
||||||
|
$item_id = $item_id ? ' id="' . esc_attr( $item_id ) . '"' : '';
|
||||||
|
|
||||||
|
$output .= $indent . '<li' . $item_id . $class_names . '>';
|
||||||
|
|
||||||
|
$atts = array(
|
||||||
|
'title' => ! empty( $item->attr_title ) ? $item->attr_title : '',
|
||||||
|
'target' => ! empty( $item->target ) ? $item->target : '',
|
||||||
|
'rel' => ! empty( $item->xfn ) ? $item->xfn : '',
|
||||||
|
'href' => ! empty( $item->url ) ? $item->url : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
|
||||||
|
$attributes = '';
|
||||||
|
|
||||||
|
foreach ( $atts as $attr => $value ) {
|
||||||
|
if ( ! empty( $value ) ) {
|
||||||
|
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
|
||||||
|
$attributes .= ' ' . $attr . '="' . $value . '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = apply_filters( 'the_title', $item->title, $item->ID );
|
||||||
|
|
||||||
|
$link = $args->before;
|
||||||
|
$link .= '<a' . $attributes . '>';
|
||||||
|
$link .= $args->link_before . $title . $args->link_after;
|
||||||
|
$link .= '</a>';
|
||||||
|
$link .= $args->after;
|
||||||
|
|
||||||
|
$output .= $link . "</li>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback menu when no primary nav is assigned in WP admin.
|
||||||
|
*
|
||||||
|
* Previously lived inline inside header.php; moved to inc/helpers.php in
|
||||||
|
* v1.5.1 to prevent redeclaration, then extracted here in v2.3.0.
|
||||||
|
*
|
||||||
|
* @return void Echoes a complete <ul>.
|
||||||
|
*/
|
||||||
|
function peptide_starter_primary_menu_fallback() {
|
||||||
|
?>
|
||||||
|
<ul>
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Home', 'peptide-starter' ); ?></a></li>
|
||||||
|
<li class="menu-item-has-children">
|
||||||
|
<a href="#"><?php esc_html_e( 'Tools', 'peptide-starter' ); ?></a>
|
||||||
|
<ul class="sub-menu">
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/calculator' ) ); ?>"><?php esc_html_e( 'Calculator', 'peptide-starter' ); ?></a></li>
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/protocol-builder' ) ); ?>"><?php esc_html_e( 'Protocol Builder', 'peptide-starter' ); ?></a></li>
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/tracker' ) ); ?>"><?php esc_html_e( 'Tracker', 'peptide-starter' ); ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item-has-children">
|
||||||
|
<a href="#"><?php esc_html_e( 'My Data', 'peptide-starter' ); ?></a>
|
||||||
|
<ul class="sub-menu">
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>"><?php esc_html_e( 'Peptides', 'peptide-starter' ); ?></a></li>
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/subject-log' ) ); ?>"><?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item-has-children">
|
||||||
|
<a href="#"><?php esc_html_e( 'Resources', 'peptide-starter' ); ?></a>
|
||||||
|
<ul class="sub-menu">
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>"><?php esc_html_e( 'Documentation', 'peptide-starter' ); ?></a></li>
|
||||||
|
<li><a href="<?php echo esc_url( home_url( '/news' ) ); ?>"><?php esc_html_e( 'Science Feed', 'peptide-starter' ); ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
137
inc/verdict-helpers.php
Normal file
137
inc/verdict-helpers.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Verdict helper functions for homepage and template parts.
|
||||||
|
*
|
||||||
|
* What: Provides data arrays for the 5-state verdict taxonomy used in the
|
||||||
|
* homepage explainer strip and featured verdict cards.
|
||||||
|
* Who triggers: Required by functions.php on every page load.
|
||||||
|
* Depends on: Nothing — pure data functions, no WP dependencies.
|
||||||
|
*
|
||||||
|
* @see inc/verdict-meta.php — verdict CPT meta registration
|
||||||
|
* @see template-parts/verdict/ — badge, card, evidence-row components
|
||||||
|
* @see front-page.php — homepage verdict sections
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the verdict taxonomy data array for the homepage explainer strip.
|
||||||
|
*
|
||||||
|
* 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), abbrs (array, optional).
|
||||||
|
*/
|
||||||
|
function peptide_starter_get_verdict_taxonomy() {
|
||||||
|
return array(
|
||||||
|
array(
|
||||||
|
'state' => 'established',
|
||||||
|
'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',
|
||||||
|
'glyph' => '◐',
|
||||||
|
'label' => 'Promising',
|
||||||
|
'description' => 'Meaningful human data accumulating; not yet RCT-strong.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'state' => 'investigational',
|
||||||
|
'glyph' => '?',
|
||||||
|
'label' => 'Investigational',
|
||||||
|
'description' => 'Mechanism plausible; human trials sparse.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'state' => 'insufficient',
|
||||||
|
'glyph' => '⊘',
|
||||||
|
'label' => 'Insufficient Evidence',
|
||||||
|
'description' => 'Not enough data to say anything responsible.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'state' => 'cautionary',
|
||||||
|
'glyph' => '⚠',
|
||||||
|
'label' => 'Cautionary',
|
||||||
|
'description' => 'Active safety signals or misuse patterns.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the verdict state config map (glyph + label per state slug).
|
||||||
|
*
|
||||||
|
* Used by featured verdict cards on the homepage and any template that needs
|
||||||
|
* to render a verdict badge from a state string without loading the full card.
|
||||||
|
*
|
||||||
|
* @return array Keyed by state slug. Each value: array{ glyph: string, label: string }.
|
||||||
|
*/
|
||||||
|
function peptide_starter_get_verdict_config() {
|
||||||
|
return array(
|
||||||
|
'established' => array(
|
||||||
|
'glyph' => '✓',
|
||||||
|
'label' => 'Established',
|
||||||
|
),
|
||||||
|
'promising' => array(
|
||||||
|
'glyph' => '◐',
|
||||||
|
'label' => 'Promising',
|
||||||
|
),
|
||||||
|
'investigational' => array(
|
||||||
|
'glyph' => '?',
|
||||||
|
'label' => 'Investigational',
|
||||||
|
),
|
||||||
|
'insufficient' => array(
|
||||||
|
'glyph' => '⊘',
|
||||||
|
'label' => 'Insufficient Evidence',
|
||||||
|
),
|
||||||
|
'cautionary' => array(
|
||||||
|
'glyph' => '⚠',
|
||||||
|
'label' => 'Cautionary',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ function peptide_starter_register_verdict_meta() {
|
|||||||
'verdict_state',
|
'verdict_state',
|
||||||
array(
|
array(
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'single' => true,
|
'single' => true,
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
'auth_callback' => '__return_true',
|
'auth_callback' => '__return_true',
|
||||||
'sanitize_callback' => function ( $value ) use ( $allowed_states ) {
|
'sanitize_callback' => function ( $value ) use ( $allowed_states ) {
|
||||||
@@ -49,7 +49,7 @@ function peptide_starter_register_verdict_meta() {
|
|||||||
'verdict_text',
|
'verdict_text',
|
||||||
array(
|
array(
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'single' => true,
|
'single' => true,
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
'auth_callback' => '__return_true',
|
'auth_callback' => '__return_true',
|
||||||
'sanitize_callback' => 'sanitize_textarea_field',
|
'sanitize_callback' => 'sanitize_textarea_field',
|
||||||
@@ -63,7 +63,7 @@ function peptide_starter_register_verdict_meta() {
|
|||||||
'signal_row_' . $i,
|
'signal_row_' . $i,
|
||||||
array(
|
array(
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'single' => true,
|
'single' => true,
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
'auth_callback' => '__return_true',
|
'auth_callback' => '__return_true',
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
@@ -77,7 +77,7 @@ function peptide_starter_register_verdict_meta() {
|
|||||||
'signal_row_' . $i . '_glyph',
|
'signal_row_' . $i . '_glyph',
|
||||||
array(
|
array(
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'single' => true,
|
'single' => true,
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
'auth_callback' => '__return_true',
|
'auth_callback' => '__return_true',
|
||||||
'sanitize_callback' => function ( $value ) use ( $allowed_glyphs ) {
|
'sanitize_callback' => function ( $value ) use ( $allowed_glyphs ) {
|
||||||
@@ -94,7 +94,7 @@ function peptide_starter_register_verdict_meta() {
|
|||||||
'has_partner_link',
|
'has_partner_link',
|
||||||
array(
|
array(
|
||||||
'type' => 'boolean',
|
'type' => 'boolean',
|
||||||
'single' => true,
|
'single' => true,
|
||||||
'show_in_rest' => true,
|
'show_in_rest' => true,
|
||||||
'auth_callback' => '__return_true',
|
'auth_callback' => '__return_true',
|
||||||
'default' => false,
|
'default' => false,
|
||||||
@@ -168,10 +168,11 @@ function peptide_starter_render_verdict_meta_box( $post ) {
|
|||||||
<hr style="border:none;border-top:1px solid #ddd;margin:8px 0 16px;">
|
<hr style="border:none;border-top:1px solid #ddd;margin:8px 0 16px;">
|
||||||
<p style="font-weight:bold;margin-bottom:8px;"><?php esc_html_e( 'Evidence Signal Rows', 'peptide-starter' ); ?></p>
|
<p style="font-weight:bold;margin-bottom:8px;"><?php esc_html_e( 'Evidence Signal Rows', 'peptide-starter' ); ?></p>
|
||||||
|
|
||||||
<?php for ( $i = 1; $i <= 3; $i++ ) :
|
<?php
|
||||||
$row_text = get_post_meta( $post->ID, 'signal_row_' . $i, true );
|
for ( $i = 1; $i <= 3; $i++ ) :
|
||||||
$row_glyph = get_post_meta( $post->ID, 'signal_row_' . $i . '_glyph', true );
|
$row_text = get_post_meta( $post->ID, 'signal_row_' . $i, true );
|
||||||
?>
|
$row_glyph = get_post_meta( $post->ID, 'signal_row_' . $i . '_glyph', true );
|
||||||
|
?>
|
||||||
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:12px;">
|
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:12px;">
|
||||||
<div style="flex:0 0 80px;">
|
<div style="flex:0 0 80px;">
|
||||||
<label for="signal_row_<?php echo esc_attr( $i ); ?>_glyph"
|
<label for="signal_row_<?php echo esc_attr( $i ); ?>_glyph"
|
||||||
|
|||||||
13
package-lock.json
generated
Normal file
13
package-lock.json
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "peptide-starter-theme",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "peptide-starter-theme",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"license": "GPL-2.0-or-later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
package.json
Normal file
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "peptide-starter-theme",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"description": "WordPress theme for peptiderepo.com",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"lint": "find assets/js -name '*.js' -print0 | xargs -0 -I{} node --check {}"
|
||||||
|
},
|
||||||
|
"license": "GPL-2.0-or-later"
|
||||||
|
}
|
||||||
@@ -33,8 +33,8 @@ get_header();
|
|||||||
<section class="ps-tool-page">
|
<section class="ps-tool-page">
|
||||||
<div class="ps-container">
|
<div class="ps-container">
|
||||||
<?php
|
<?php
|
||||||
if ( shortcode_exists( 'peptide_tools_calculator' ) ) {
|
if ( shortcode_exists( 'prc_calculator' ) ) {
|
||||||
echo do_shortcode( '[peptide_tools_calculator]' );
|
echo do_shortcode( '[prc_calculator]' );
|
||||||
} else {
|
} else {
|
||||||
?>
|
?>
|
||||||
<div class="ps-tool-fallback">
|
<div class="ps-tool-fallback">
|
||||||
|
|||||||
46
page-our-methodology.php
Normal file
46
page-our-methodology.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Template Name: Our Methodology
|
||||||
|
*
|
||||||
|
* Two-column layout with sticky sidebar ToC for the editorial methodology page.
|
||||||
|
* Mirrors page-how-we-review.php layout; CSS classes use page-our-methodology prefix.
|
||||||
|
*
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
get_header();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main id="main" class="site-main">
|
||||||
|
<div class="page-our-methodology">
|
||||||
|
<div class="page-our-methodology__content">
|
||||||
|
<?php
|
||||||
|
while ( have_posts() ) {
|
||||||
|
the_post();
|
||||||
|
?>
|
||||||
|
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
||||||
|
<header class="entry-header">
|
||||||
|
<h1 class="entry-title"><?php the_title(); ?></h1>
|
||||||
|
</header>
|
||||||
|
<div class="entry-content">
|
||||||
|
<?php the_content(); ?>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( get_the_content() ) ) : ?>
|
||||||
|
<aside class="page-our-methodology__sidebar">
|
||||||
|
<nav class="page-toc" aria-label="<?php esc_attr_e( 'Page contents', 'peptide-starter' ); ?>">
|
||||||
|
<h3><?php esc_html_e( 'Contents', 'peptide-starter' ); ?></h3>
|
||||||
|
<!-- JavaScript-generated ToC will be inserted here -->
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
get_footer();
|
||||||
44
page-subscribe.php
Executable file
44
page-subscribe.php
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Template Name: Subscribe
|
||||||
|
*
|
||||||
|
* Single-column, signup-form-centric layout for the /subscribe/ landing page.
|
||||||
|
* Lighter than the methodology / how-we-review templates — no sticky two-column ToC.
|
||||||
|
* This is a conversion page, not a reference page.
|
||||||
|
*
|
||||||
|
* Who calls it: WordPress page loader when page_template meta = page-subscribe.php
|
||||||
|
* Dependencies: Beehiiv embed script (loaded inline below); attribution.js loaded
|
||||||
|
* site-wide via peptide_starter_enqueue_beehiiv_attribution() in functions.php
|
||||||
|
*
|
||||||
|
* @see footer.php — newsletter CTA in footer column 1
|
||||||
|
* @see single-peptide.php — compact inline form on monograph pages
|
||||||
|
* @package peptide-starter
|
||||||
|
*/
|
||||||
|
|
||||||
|
get_header();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main id="main" class="site-main">
|
||||||
|
<div class="page-subscribe">
|
||||||
|
<div class="page-subscribe__content">
|
||||||
|
<?php
|
||||||
|
while ( have_posts() ) {
|
||||||
|
the_post();
|
||||||
|
?>
|
||||||
|
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
||||||
|
<header class="entry-header">
|
||||||
|
<h1 class="entry-title"><?php the_title(); ?></h1>
|
||||||
|
</header>
|
||||||
|
<div class="entry-content">
|
||||||
|
<?php the_content(); ?>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
get_footer();
|
||||||
@@ -2,18 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* The single peptide template
|
* The single peptide template
|
||||||
*
|
*
|
||||||
* Displays a single peptide with verdict card hero, content, affiliate
|
* Displays a single peptide with back link, content box,
|
||||||
* disclosure, and meta properties in a card grid layout.
|
* and meta information in a card grid layout.
|
||||||
*
|
|
||||||
* When a verdict_state is set on the post, the verdict card owns the <h1>
|
|
||||||
* title so the page-header section only renders the back-link and category
|
|
||||||
* pills (no duplicate heading). When no verdict state exists the classic <h1>
|
|
||||||
* renders in the header for backwards compatibility with non-monograph posts.
|
|
||||||
*
|
|
||||||
* Who calls it: WordPress template hierarchy for the pr_peptide CPT.
|
|
||||||
* Depends on: inc/verdict-meta.php (post meta registration),
|
|
||||||
* template-parts/verdict/card.php,
|
|
||||||
* template-parts/affiliate-disclosure.php.
|
|
||||||
*
|
*
|
||||||
* @package peptide-starter
|
* @package peptide-starter
|
||||||
*/
|
*/
|
||||||
@@ -26,9 +16,6 @@ get_header();
|
|||||||
if ( have_posts() ) {
|
if ( have_posts() ) {
|
||||||
while ( have_posts() ) {
|
while ( have_posts() ) {
|
||||||
the_post();
|
the_post();
|
||||||
|
|
||||||
// Determine verdict state once — used in two places below.
|
|
||||||
$verdict_state = get_post_meta( get_the_ID(), 'verdict_state', true );
|
|
||||||
?>
|
?>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
@@ -41,7 +28,7 @@ get_header();
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Show featured badge if the peptide is marked as featured.
|
// Show featured badge if the peptide is marked as featured
|
||||||
if ( is_sticky() || get_post_meta( get_the_ID(), 'peptide_featured', true ) ) {
|
if ( is_sticky() || get_post_meta( get_the_ID(), 'peptide_featured', true ) ) {
|
||||||
?>
|
?>
|
||||||
<span class="ps-featured-badge">★ <?php esc_html_e( 'Featured', 'peptide-starter' ); ?></span>
|
<span class="ps-featured-badge">★ <?php esc_html_e( 'Featured', 'peptide-starter' ); ?></span>
|
||||||
@@ -49,19 +36,10 @@ get_header();
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php
|
<h1 class="page-title"><?php the_title(); ?></h1>
|
||||||
// Only render the page-level <h1> when no verdict card is present.
|
|
||||||
// The verdict card component owns the <h1> for monograph posts so
|
|
||||||
// that readers encounter a single, semantically correct heading.
|
|
||||||
if ( empty( $verdict_state ) ) {
|
|
||||||
?>
|
|
||||||
<h1 class="page-title"><?php the_title(); ?></h1>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Category tag pills.
|
// Category tag pills
|
||||||
$categories = get_the_terms( get_the_ID(), 'peptide-category' );
|
$categories = get_the_terms( get_the_ID(), 'peptide-category' );
|
||||||
if ( $categories && ! is_wp_error( $categories ) ) {
|
if ( $categories && ! is_wp_error( $categories ) ) {
|
||||||
echo '<div class="peptide-card__meta" style="margin-top: var(--spacing-sm);">';
|
echo '<div class="peptide-card__meta" style="margin-top: var(--spacing-sm);">';
|
||||||
@@ -74,17 +52,6 @@ get_header();
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Verdict Card Hero (monograph posts only) -->
|
|
||||||
<?php
|
|
||||||
if ( ! empty( $verdict_state ) ) {
|
|
||||||
get_template_part(
|
|
||||||
'template-parts/verdict/card',
|
|
||||||
null,
|
|
||||||
array( 'post_id' => get_the_ID() )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Single Peptide Content -->
|
<!-- Single Peptide Content -->
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<article class="peptide-single" id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
<article class="peptide-single" id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
||||||
@@ -102,22 +69,6 @@ get_header();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
|
||||||
// Inline disclosure renders after the content box (below any partner
|
|
||||||
// links in the_content). Conditional on has_partner_link meta — if no
|
|
||||||
// partner link exists on this monograph, the disclosure is suppressed.
|
|
||||||
// Default false: new monographs are disclosure-off until a partner is
|
|
||||||
// added and the meta is explicitly set to true by the CMO.
|
|
||||||
$has_partner_link = (bool) get_post_meta( get_the_ID(), 'has_partner_link', true );
|
|
||||||
if ( $verdict_state && $has_partner_link ) {
|
|
||||||
get_template_part(
|
|
||||||
'template-parts/affiliate-disclosure',
|
|
||||||
null,
|
|
||||||
array( 'context' => 'inline' )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!-- Meta Information as card grid -->
|
<!-- Meta Information as card grid -->
|
||||||
<?php
|
<?php
|
||||||
$formula = get_post_meta( get_the_ID(), 'peptide_formula', true );
|
$formula = get_post_meta( get_the_ID(), 'peptide_formula', true );
|
||||||
@@ -188,6 +139,25 @@ get_header();
|
|||||||
?>
|
?>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<!-- Newsletter Signup — compact inline form below monograph content -->
|
||||||
|
<div class="peptide-newsletter-cta" style="margin-top: var(--spacing-3xl); padding: var(--spacing-2xl); background: var(--color-surface-subtle, #f8f8f6); border-radius: 8px; border: 1px solid var(--color-border-default);">
|
||||||
|
<p style="margin: 0 0 var(--spacing-md); font-size: 0.95rem; color: var(--color-text-secondary);">
|
||||||
|
<?php esc_html_e( 'Want updates on monographs like this? One email a week, no spam.', 'peptide-starter' ); ?>
|
||||||
|
</p>
|
||||||
|
<div class="beehiiv-embed-wrap" style="max-width: 100%; overflow: hidden;">
|
||||||
|
<script async src="https://subscribe-forms.beehiiv.com/embed.js"></script>
|
||||||
|
<iframe
|
||||||
|
src="https://subscribe-forms.beehiiv.com/6edcd482-cbea-4767-8777-97ac17d6d609"
|
||||||
|
class="beehiiv-embed"
|
||||||
|
data-test-id="beehiiv-embed"
|
||||||
|
frameborder="0"
|
||||||
|
scrolling="no"
|
||||||
|
style="width: 100%; max-width: 100%; height: 180px; margin: 0; border-radius: 4px; background-color: transparent; box-shadow: 0 0 #0000;"
|
||||||
|
title="<?php esc_attr_e( 'Subscribe to Repo Weekly', 'peptide-starter' ); ?>"
|
||||||
|
> </iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="post-navigation" style="margin-top: var(--spacing-3xl); padding-top: var(--spacing-2xl); border-top: 1px solid var(--color-border-default);">
|
<nav class="post-navigation" style="margin-top: var(--spacing-3xl); padding-top: var(--spacing-2xl); border-top: 1px solid var(--color-border-default);">
|
||||||
<div style="display: flex; justify-content: space-between; gap: var(--spacing-lg);">
|
<div style="display: flex; justify-content: space-between; gap: var(--spacing-lg);">
|
||||||
|
|||||||
300
style.css
300
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.0.0-alpha.3
|
Version: 2.3.0
|
||||||
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
|
||||||
@@ -1674,6 +1674,13 @@ body .pn-article-title {
|
|||||||
margin-bottom: var(--spacing-sm);
|
margin-bottom: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapse empty Elementor/page-builder containers that add phantom space */
|
||||||
|
.page-content .elementor-section:empty,
|
||||||
|
.page-content .elementor-widget-wrap:empty,
|
||||||
|
.page-content .elementor-element:empty,
|
||||||
|
.page-content div:empty:not([data-peptide-news-feed]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- BACK LINK (breadcrumb-style) ---- */
|
/* ---- BACK LINK (breadcrumb-style) ---- */
|
||||||
|
|
||||||
@@ -3164,3 +3171,294 @@ html[data-theme="dark"] .ps-verify-required {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
HOMEPAGE — VERDICT EXPLAINER SECTION
|
||||||
|
hero-verdict-explainer: "Every peptide gets a verdict. Here's what they mean."
|
||||||
|
5-state explainer grid above the featured verdict cards.
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.hero-verdict-explainer {
|
||||||
|
padding: var(--spacing-3xl) 0; /* horizontal padding handled by ps-container inside */
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__heading {
|
||||||
|
max-width: var(--container-lg);
|
||||||
|
margin: 0 auto var(--spacing-xl);
|
||||||
|
font-size: var(--text-h3);
|
||||||
|
font-weight: var(--text-h3-weight);
|
||||||
|
line-height: var(--text-h3-lh);
|
||||||
|
letter-spacing: var(--text-h3-ls);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__grid {
|
||||||
|
max-width: var(--container-lg);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr; /* single column on narrow phones; 2-col added at 520px below */
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
min-width: 0; /* prevent grid item from blowing out column */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cap pill width to column so Insufficient Evidence pill never overflows */
|
||||||
|
.hero-verdict-explainer__item .pr-verdict {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__desc {
|
||||||
|
font-size: var(--text-body-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__cta-line {
|
||||||
|
max-width: var(--container-lg);
|
||||||
|
margin: var(--spacing-xl) auto 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__cta-line a {
|
||||||
|
color: var(--color-brand-primary);
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__cta-line a:hover {
|
||||||
|
color: var(--color-link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
HOMEPAGE — FEATURED VERDICT CARDS SECTION
|
||||||
|
hero-featured-verdicts: 3 pinned monographs showing the verdict system in action.
|
||||||
|
Classes mirror BEM pattern used in front-page.php.
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.hero-featured-verdicts {
|
||||||
|
padding: var(--spacing-3xl) var(--spacing-lg);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-featured-verdicts__title {
|
||||||
|
font-size: var(--text-h2);
|
||||||
|
font-weight: var(--text-h2-weight);
|
||||||
|
line-height: var(--text-h2-lh);
|
||||||
|
letter-spacing: var(--text-h2-ls);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-featured-verdicts__subtitle {
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-featured-verdicts__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual verdict card */
|
||||||
|
.hero-verdict-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-left: 3px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge sits at top of card — already styled by .pr-verdict in brand.css */
|
||||||
|
.hero-verdict-card__badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tint left border to match verdict state — uses CSS :has() (supported in all modern browsers) */
|
||||||
|
.hero-verdict-card:has(.pr-verdict--established) {
|
||||||
|
border-left-color: var(--verdict-established-bg, #059669);
|
||||||
|
}
|
||||||
|
.hero-verdict-card:has(.pr-verdict--promising) {
|
||||||
|
border-left-color: var(--verdict-promising-bg, #10b981);
|
||||||
|
}
|
||||||
|
.hero-verdict-card:has(.pr-verdict--investigational) {
|
||||||
|
border-left-color: var(--verdict-investigational-bg, #f59e0b);
|
||||||
|
}
|
||||||
|
.hero-verdict-card:has(.pr-verdict--insufficient) {
|
||||||
|
border-left-color: var(--verdict-insufficient-bg, #6b7280);
|
||||||
|
}
|
||||||
|
.hero-verdict-card:has(.pr-verdict--cautionary) {
|
||||||
|
border-left-color: var(--verdict-cautionary-bg, #dc2626);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__title {
|
||||||
|
font-size: var(--text-h3);
|
||||||
|
font-weight: var(--text-h3-weight);
|
||||||
|
line-height: var(--text-h3-lh);
|
||||||
|
letter-spacing: var(--text-h3-ls);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__title a {
|
||||||
|
color: var(--color-brand-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__title a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
color: var(--color-link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__summary {
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signal rows list */
|
||||||
|
.hero-verdict-card__signals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background-color: var(--color-bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__signal-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
font-size: var(--text-body-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__signal-glyph {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Read the full evidence →" link */
|
||||||
|
.hero-verdict-card__link {
|
||||||
|
align-self: flex-start;
|
||||||
|
color: var(--color-brand-primary);
|
||||||
|
font-size: var(--text-body-lg);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-card__link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
color: var(--color-link-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Browse all peptides" footer row */
|
||||||
|
.hero-featured-verdicts__footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- RESPONSIVE — Featured Verdict Cards ---- */
|
||||||
|
|
||||||
|
@media (min-width: 520px) {
|
||||||
|
.hero-verdict-explainer__grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr); /* 520px+ gives columns wide enough for all pills */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.hero-verdict-explainer__grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.hero-featured-verdicts__grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-verdict-explainer__grid {
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
/**
|
/**
|
||||||
* Template Part: Research Module Cards
|
* Template Part: Research Module Cards
|
||||||
*
|
*
|
||||||
* Renders a 6-card grid of research modules (tools/sections) for the front page.
|
* Renders a 5-card grid of research modules (tools/sections) for the front page.
|
||||||
* Each card checks if the target plugin is active; inactive cards are dimmed.
|
* Each card checks if the target plugin is active; inactive cards are dimmed.
|
||||||
|
* Cards with 'coming_soon' => true display a "Coming soon" badge overlaid in
|
||||||
|
* the top-right corner — layered on top of the --inactive visual treatment.
|
||||||
*
|
*
|
||||||
* @see front-page.php — includes this template part
|
* @see front-page.php — includes this template part
|
||||||
* @see style.css — .ps-module-* classes
|
* @see style.css — .ps-module-* classes
|
||||||
@@ -21,48 +23,48 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Module card definitions with shortcode checks for graceful degradation.
|
// Module card definitions with shortcode checks for graceful degradation.
|
||||||
|
// 'coming_soon' => true adds a "Coming soon" badge without removing existing
|
||||||
|
// --inactive styling — the badge layers on top of the dim/no-hover treatment.
|
||||||
$modules = array(
|
$modules = array(
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Peptides', 'peptide-starter' ),
|
'title' => __( 'Peptides', 'peptide-starter' ),
|
||||||
'description' => __( 'Browse the complete reagent directory with protocols and research parameters.', 'peptide-starter' ),
|
'description' => __( 'Read the verdicts. 20 monographs across 5 evidence states, growing weekly.', 'peptide-starter' ),
|
||||||
'url' => '/peptides',
|
'url' => '/peptides',
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"/><path d="M5 12C5 8.13 8.13 5 12 5s7 3.13 7 7-3.13 7-7 7-7-3.13-7-7z"/><circle cx="12" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="16" r="1.5" fill="currentColor" stroke="none"/></svg>',
|
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"/><path d="M5 12C5 8.13 8.13 5 12 5s7 3.13 7 7-3.13 7-7 7-7-3.13-7-7z"/><circle cx="12" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="16" r="1.5" fill="currentColor" stroke="none"/></svg>',
|
||||||
'available' => shortcode_exists( 'peptide_directory' ) || post_type_exists( 'peptide' ),
|
'available' => shortcode_exists( 'peptide_directory' ) || post_type_exists( 'peptide' ),
|
||||||
|
'coming_soon' => false,
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Calculator', 'peptide-starter' ),
|
'title' => __( 'Calculator', 'peptide-starter' ),
|
||||||
'description' => __( 'Precision reconstitution calculator for accurate laboratory preparations.', 'peptide-starter' ),
|
'description' => __( 'Reconstitute a peptide accurately. Enter the vial, the bacteriostatic water, the target dose; get the units.', 'peptide-starter' ),
|
||||||
'url' => '/calculator',
|
'url' => '/calculator',
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>',
|
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>',
|
||||||
'available' => shortcode_exists( 'peptide_tools_calculator' ),
|
'available' => shortcode_exists( 'prc_calculator' ),
|
||||||
|
'coming_soon' => false,
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Protocol Builder', 'peptide-starter' ),
|
'title' => __( 'Protocol Builder', 'peptide-starter' ),
|
||||||
'description' => __( 'Interactive multi-step wizard for designing research protocols.', 'peptide-starter' ),
|
'description' => __( 'Build a protocol you can hand to anyone. Doses, schedule, notes, reviewable history.', 'peptide-starter' ),
|
||||||
'url' => '/protocol-builder',
|
'url' => '/protocol-builder',
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>',
|
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>',
|
||||||
'available' => shortcode_exists( 'peptide_tools_protocol_builder' ),
|
'available' => shortcode_exists( 'peptide_tools_protocol_builder' ),
|
||||||
|
'coming_soon' => true,
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Tracker', 'peptide-starter' ),
|
'title' => __( 'Tracker', 'peptide-starter' ),
|
||||||
'description' => __( 'Log doses, manage active compounds, and forecast vial depletion timelines.', 'peptide-starter' ),
|
'description' => __( 'Log doses and timing. For people running their own protocols, and people supervising someone else\'s.', 'peptide-starter' ),
|
||||||
'url' => '/tracker',
|
'url' => '/tracker',
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>',
|
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>',
|
||||||
'available' => shortcode_exists( 'peptide_tracker' ),
|
'available' => shortcode_exists( 'peptide_tracker' ),
|
||||||
),
|
'coming_soon' => true,
|
||||||
array(
|
|
||||||
'title' => __( 'Science Feed', 'peptide-starter' ),
|
|
||||||
'description' => __( 'Latest research publications, clinical studies, and laboratory insights.', 'peptide-starter' ),
|
|
||||||
'url' => '/news',
|
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 002-2V4a2 2 0 00-2-2H8a2 2 0 00-2 2v16a2 2 0 01-2 2zm0 0a2 2 0 01-2-2v-9c0-1.1.9-2 2-2h2"/><line x1="10" y1="6" x2="18" y2="6"/><line x1="10" y1="10" x2="18" y2="10"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
|
|
||||||
'available' => shortcode_exists( 'peptide_news' ),
|
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'title' => __( 'Subject Log', 'peptide-starter' ),
|
'title' => __( 'Subject Log', 'peptide-starter' ),
|
||||||
'description' => __( 'Longitudinal tracking for biomarker data and protocol history.', 'peptide-starter' ),
|
'description' => __( 'Track outcomes over time. Subjective markers, lab results, protocol changes — your own data, kept private.', 'peptide-starter' ),
|
||||||
'url' => '/subject-log',
|
'url' => '/subject-log',
|
||||||
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
||||||
'available' => shortcode_exists( 'peptide_tracker_subject_log' ),
|
'available' => shortcode_exists( 'peptide_tracker_subject_log' ),
|
||||||
|
'coming_soon' => true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
@@ -70,9 +72,9 @@ $modules = array(
|
|||||||
<section class="ps-modules-section">
|
<section class="ps-modules-section">
|
||||||
<div class="ps-container-wide">
|
<div class="ps-container-wide">
|
||||||
<div class="ps-modules-header">
|
<div class="ps-modules-header">
|
||||||
<h2 class="ps-modules-title"><?php esc_html_e( 'Research Modules', 'peptide-starter' ); ?></h2>
|
<h2 class="ps-modules-title"><?php esc_html_e( 'Tools', 'peptide-starter' ); ?></h2>
|
||||||
<p class="ps-modules-subtitle">
|
<p class="ps-modules-subtitle">
|
||||||
<?php esc_html_e( 'Tools and resources for laboratory research and data management.', 'peptide-starter' ); ?>
|
<?php esc_html_e( 'Free to use. Built for people running their own protocols, and people deciding whether they should.', 'peptide-starter' ); ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -80,6 +82,9 @@ $modules = array(
|
|||||||
<?php foreach ( $modules as $module ) : ?>
|
<?php foreach ( $modules as $module ) : ?>
|
||||||
<a href="<?php echo esc_url( home_url( $module['url'] ) ); ?>"
|
<a href="<?php echo esc_url( home_url( $module['url'] ) ); ?>"
|
||||||
class="ps-module-card <?php echo $module['available'] ? '' : 'ps-module-card--inactive'; ?>">
|
class="ps-module-card <?php echo $module['available'] ? '' : 'ps-module-card--inactive'; ?>">
|
||||||
|
<?php if ( ! empty( $module['coming_soon'] ) ) : ?>
|
||||||
|
<span class="ps-module-card__badge"><?php esc_html_e( 'Coming soon', 'peptide-starter' ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="ps-module-card__icon">
|
<div class="ps-module-card__icon">
|
||||||
<?php
|
<?php
|
||||||
// SVG icons are defined in this template — safe to output directly.
|
// SVG icons are defined in this template — safe to output directly.
|
||||||
@@ -96,3 +101,4 @@ $modules = array(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user