<?php
|
namespace JVBase\managers;
|
|
use WP_User;
|
|
/**
|
* Edmonton Ink Email Customization
|
*
|
* This file customizes all default WordPress emails to match the Edmonton Ink brand aesthetic.
|
* - Sets emails to HTML format
|
* - Applies consistent branding (logo, colors, typography)
|
* - Customizes content for each email type
|
*
|
*/
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Hooks into wp_mail to ensure a consistent look in our automatic emails
|
*/
|
|
class EmailManager
|
{
|
|
public array $colours;
|
private string $title = JVB_EMAIL['content']['title'];
|
private string $prefix = JVB_EMAIL['content']['subjectPrefix'];
|
private string $signature = JVB_EMAIL['content']['signature'];
|
private string $footer;
|
|
// Site info
|
private string $site_name;
|
private string $site_url;
|
|
/**
|
* Constructor - sets up all filters
|
*/
|
public function __construct()
|
{
|
$this->site_name = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
|
$this->site_url = get_site_url();
|
$this->footer = (is_array(JVB_EMAIL['content']['footer'])) ? implode('', JVB_EMAIL['content']['footer']) : JVB_EMAIL['content']['footer'];
|
$this->colours = JVB_COLOURS;
|
add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
|
// User registration emails
|
add_filter('wp_new_user_notification_email', [$this, 'customizeNewUserEmail'], 999, 3);
|
add_filter('wp_new_user_notification_email_admin', [$this, 'customizeNewUserEmailAdmin'], 999, 3);
|
|
// Password reset emails
|
add_filter('retrieve_password_message', [$this, 'customizePasswordResetEmail'], 999, 4);
|
add_filter('retrieve_password_title', [$this, 'customizePasswordResetTitle'], 999, 3);
|
|
// User email change emails
|
add_filter('email_change_email', [$this, 'customizeEmailChangeEmail'], 999, 3);
|
add_filter('new_user_email_content', [$this, 'customizeNewUserEmailContent'], 999, 2);
|
|
// Password change notification
|
add_filter('password_change_email', [$this, 'customizePasswordChangeEmail'], 999, 3);
|
|
// User request/export data emails
|
add_filter('user_request_action_email_content', [$this, 'customizeUserRequestEmail'], 999, 2);
|
add_filter('wp_privacy_personal_data_email_content', [$this, 'customizePersonalDataEmail'], 999, 3);
|
}
|
|
/**
|
* Helper to set content type to HTML
|
* @return string
|
*/
|
public function setHtmlContentType():string
|
{
|
return 'text/html';
|
}
|
|
/**
|
* Send a styled email using the common template
|
*
|
* @param string $to Recipient email address
|
* @param string $subject Email subject line
|
* @param string $message Email body content (can contain HTML)
|
* @param string $header Optional header text for the template
|
* @return bool Whether the email was sent successfully
|
*/
|
public function sendEmail(string $to, string $subject, string $message, string $header = '', string $preheader = '', array $headers = [], array $attachments = []):bool
|
{
|
// Make sure the content type is set to HTML
|
add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
|
|
// Format the message with our template
|
$formatted_message = $this->getEmailTemplate($message, $header, $preheader);
|
|
// Send the email
|
$result = wp_mail($to, $subject, $formatted_message, $headers, $attachments);
|
|
// Reset content type filter to avoid affecting other emails
|
remove_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
|
|
return $result;
|
}
|
/**
|
* Common email wrapper template
|
* @param string $content
|
* @param string $header
|
*
|
* @return string
|
*/
|
private function getEmailTemplate(string $content, string $headerText = '', string $preheader = ''):string
|
{
|
$custom_logo_id = get_theme_mod( 'custom_logo' );
|
$logo_thumbnail = wp_get_attachment_image_src( $custom_logo_id, 'custom-logo-thumbnail' );
|
|
$logo = ($logo_thumbnail) ? '<img src="' . esc_url( $logo_thumbnail[0] ) . '" alt="Site Logo" width="150" height="150" style="max-width:120px;height:auto;">' : '';
|
|
|
// Default header if none provided
|
if (empty($headerText)) {
|
$headerText = $this->title;
|
}
|
|
$preheaderHtml = '';
|
if (!empty($preheader)) {
|
$preheaderHtml = '
|
<div style="display:none;font-size:1px;color:#fefefe;line-height:1px;font-family:Helvetica,Arial,sans-serif;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">
|
' . esc_html($preheader) . '
|
</div>';
|
}
|
|
return '<!DOCTYPE html>
|
<html>
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>' . $this->title . ' | ' . $this->site_name .'</title>
|
</head>
|
<body style="font-family:Segoe UI, Tahoma, Geneva, Verdana, sans-serif;background-color: '.$this->colours['light'].';color:'.$this->colours['dark'].';line-height:1.5;margin:0;padding:0;">
|
<div style="background-color:'.$this->colours['dark'].';color:'.$this->colours['light'].';padding:5px;text-align:center;font-size:24px;font-weight:900;letter-spacing:1.5px;text-transform:uppercase;border-radius:5px 5px 0 0;">
|
<a href="' . $this->site_url . '" style="display:inline-block;vertical-align:middle;">
|
' . $logo . '
|
</a>
|
<p style="display:inline-block;vertical-align:middle;margin: 0 auto 0 0;">' . $headerText . '</p>
|
</div>
|
<div style="padding:4rem 0;width:100%;max-width:600px;margin:0 auto;">
|
' . $content . '
|
'.$this->signature().'
|
</div>
|
<div style="border-radius:0 0 5px 5px;text-align:center;margin-top:20px;font-size:12px;background-color:'.$this->colours['light-200'].'; color: '.$this->colours['dark-200'].';padding:10px;line-height:1.6;">
|
' . $this->footer . '
|
</div>
|
</body>
|
</html>';
|
}
|
|
private function oldStyle(){
|
$oldStyle ='
|
<style>
|
body, table, td, p, a, li, blockquote {
|
line-height: 1.5;
|
}
|
|
body {
|
background-color: ' . $this->colours['light'] . ';
|
margin: 0;
|
padding: 0;
|
color: ' . $this->colours['dark'] . ';
|
}
|
|
.email-container {
|
max-width: 600px;
|
margin: 0 auto;
|
padding: 20px;
|
}
|
|
.header img {
|
max-width: 120px;
|
height: auto;
|
}
|
|
.header {
|
background-color: ' . $this->colours['dark'] . ';
|
padding: 5px;
|
text-align: center;
|
color: ' . $this->colours['light'] . ';
|
font-size: 24px;
|
font-weight: 900;
|
letter-spacing: 1.5px;
|
text-transform: uppercase;
|
border-top-left-radius: 5px;
|
border-top-right-radius: 5px;
|
}
|
.header p,
|
.header a {
|
display: inline-block;
|
vertical-align: middle;
|
}
|
.header p {
|
margin: 0 auto 0 0;
|
}
|
|
.content {
|
background-color: ' . $this->colours['light-50'] . ';
|
padding: 30px;
|
border-bottom-left-radius: 5px;
|
border-bottom-right-radius: 5px;
|
}
|
|
.button {
|
display: inline-block;
|
padding: 12px 24px;
|
background-color: ' . $this->colours['action-0'] . ';
|
color: ' . $this->colours['action-contrast'] . ' !important;
|
text-decoration: none;
|
border-radius: 3px;
|
font-weight: bold;
|
margin: 15px 0;
|
letter-spacing: 1px;
|
text-transform: uppercase;
|
}
|
|
.text-link {
|
color: ' . $this->colours['action-0'] . ';
|
text-decoration: underline;
|
}
|
|
.footer {
|
text-align: center;
|
margin-top: 20px;
|
font-size: 12px;
|
background-color: ' . $this->colours['light-200'] . ';
|
color: ' . $this->colours['dark-200'] . ';
|
padding: 10px;
|
line-height: 1.6;
|
}
|
|
.notice {
|
text-align: center;
|
border-radius: 0 8px 8px 0;
|
margin: 1rem 0;
|
border-left: 4px solid '.$this->colours['action-0'].';
|
padding: 1rem;
|
background-color: '.$this->colours['light-100'].';
|
}
|
.notice strong {
|
color: '.$this->colours['action-0'].';
|
}
|
|
.divider {
|
border-top: 1px solid ' . $this->colours['dark-200'] . ';
|
margin: 25px 0;
|
}
|
|
@media screen and (max-width: 600px) {
|
.email-container {
|
width: 100% !important;
|
padding: 10px !important;
|
}
|
|
.content, .header {
|
padding: 15px !important;
|
}
|
}
|
|
@media screen and (max-width: 600px) {
|
/* Table adjustments for content rows */
|
table, tbody, tr, td {
|
display: block !important;
|
width: 100% !important;
|
clear: both !important;
|
}
|
|
/* Make images responsive */
|
img {
|
max-width: 100% !important;
|
height: auto !important;
|
}
|
|
/* Increase tap target sizes */
|
.button {
|
display: block !important;
|
text-align: center !important;
|
padding: 16px 10px !important;
|
margin: 15px auto !important;
|
width: 80% !important;
|
}
|
|
/* Typography adjustments */
|
h1 {
|
font-size: 24px !important;
|
}
|
h2 {
|
font-size: 20px !important;
|
}
|
h3 {
|
font-size: 18px !important;
|
}
|
|
/* Content spacing */
|
.content {
|
padding: 15px 10px !important;
|
}
|
|
/* Stack list items better */
|
li {
|
margin-bottom: 10px !important;
|
}
|
|
/* Adjust content cells for better stacking */
|
td {
|
padding: 10px 0 !important;
|
text-align: center !important;
|
}
|
|
/* Header adjustments */
|
.header img {
|
max-width: 100px !important;
|
}
|
|
.header p {
|
font-size: 18px !important;
|
margin: 5px 0 !important;
|
display: block !important; /* Force stacking */
|
}
|
}
|
|
/* Dark mode support for clients that support it */
|
@media (prefers-color-scheme: dark) {
|
.body {
|
background-color: ' . $this->colours['dark'] . ';
|
color: ' . $this->colours['light'] . ';
|
}
|
|
.header {
|
background-color: ' . $this->colours['light'] . ';
|
color: ' . $this->colours['dark'] . ';
|
}
|
|
.content {
|
background-color: ' . $this->colours['dark-50'] . ';
|
}
|
|
.footer {
|
background-color: ' . $this->colours['dark-200'] . ';
|
color: ' . $this->colours['light-200'] . ';
|
}
|
}
|
</style>
|
';
|
}
|
|
/**
|
* New user registration email to user
|
*/
|
public function customizeNewUserEmail($wp_new_user_notification_email, $user, $blogname)
|
{
|
$message = sprintf(
|
'<p>Hi %s!</p>
|
<p>Thanks for signing up for an account on %s.</p>
|
<p><b>Your username:</b> %s</p>
|
<p><b>Your password:</b> <i>Your chosen password.</i></p>',
|
$user->display_name,
|
get_bloginfo('name'),
|
$user->user_login,
|
);
|
$message = apply_filters('jvbNewUserEmail', $message, $user);
|
$wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['types']['newUser']['subject']?:'New User');
|
|
// Change the subject line
|
$prefix = JVB_EMAIL['types']['newUser']['showPrefix']??true;
|
$prefix = ($prefix) ? $this->prefix : '';
|
$wp_new_user_notification_email['subject'] = $prefix.JVB_EMAIL['types']['newUser']['subject']?:'New User';
|
|
return $wp_new_user_notification_email;
|
}
|
|
/**
|
* New user registration email to admin
|
* @param array $emailData
|
* @param WP_User $user
|
* @param string $blogname
|
*
|
* @return array
|
*/
|
public function customizeNewUserEmailAdmin(array $emailData, WP_User $user, string $blogname):array
|
{
|
$message = '<p>A new user has registered on <strong>' . $this->site_name . '</strong>:</p>';
|
$message .= '<p><strong>Username:</strong> ' . $user->user_login . '</p>';
|
$message .= '<p><strong>Email:</strong> ' . $user->user_email . '</p>';
|
$message = apply_filters('jvbNewUserAdminEmail', $message, $user);
|
$emailData['message'] = $this->getEmailTemplate($message, 'New User Registration');
|
$emailData['subject'] = $this->prefix .'New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name;
|
|
return $emailData;
|
}
|
|
/**
|
* Password reset email
|
* @param string $message
|
* @param string $key
|
* @param string $user_login
|
* @param WP_User $user_data
|
*
|
* @return string
|
*/
|
public function customizePasswordResetEmail(string $message, string $key, string $user_login, WP_User $user):string
|
{
|
return $this->passwordResetEmail($user, $key);
|
}
|
|
public function passwordResetEmail(WP_User $user, string $key):string {
|
$reset_url = network_site_url("login/?action=resetpass&key=$key&login=" . rawurlencode($user->user_login), 'login');
|
$content = sprintf(
|
'<p>Hi %s!</p>
|
<p>We received a request to reset the password for an account associated with this email:</p>
|
<p><strong>Username:</strong> %s</p>
|
<p>If you didn\'t make this request, you can safely ignore this email and nothing will happen to your account.</p>
|
<p>To reset your password, click the button below:</p>
|
%s
|
<p>Or copy and paste this link into your browser:</p>
|
%s
|
%s
|
<p>This password reset link is only valid for 24 hours.</p>',
|
$user->display_name,
|
$user->user_login,
|
$this->button($reset_url,'Reset Password'),
|
$this->link($reset_url),
|
$this->divider()
|
);
|
return apply_filters('jvbPasswordResetEmail', $content, $user->user_login, $user, $reset_url);
|
}
|
|
public function sendPasswordResetEmail(WP_User $user, string $key):bool
|
{
|
return $this->sendEmail($user->user_email, $this->passwordResetTitle(), $this->passwordResetEmail($user, $key), '', 'Reset your Password');
|
}
|
|
/**
|
* Customize the password reset email title
|
* @param string $title
|
* @param string $user_login
|
* @param WP_User $user_data
|
* @return string
|
*/
|
public function customizePasswordResetTitle(string $title, string $user_login, WP_User $user_data):string
|
{
|
return $this->passwordResetTitle();
|
}
|
|
public function passwordResetTitle():string
|
{
|
$prefix = JVB_EMAIL['types']['resetPass']['showPrefix']??true;
|
$prefix = ($prefix) ? $this->prefix : '';
|
return $prefix.JVB_EMAIL['types']['resetPass']['subject']?:'Password Reset';
|
}
|
|
/**
|
* Email change notification to admin
|
* @param array $email_change_email
|
* @param array $oldUser
|
* @param array $newUser
|
*
|
* @return array
|
*/
|
public function customizeEmailChangeEmail(array $email_change_email, array $oldUser, array $newUser):array
|
{
|
$content = sprintf(
|
'<p>Hi %s,</p>
|
<p>Ideally you already know this: someone asked to change the email for your account.</p>
|
<p><strong>Old Email:</strong> %s</p>
|
<p><strong>New Email:</strong> %s</p>
|
%s
|
<p>If this is news to you, or you did not request this - please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>
|
%s',
|
$newUser['first_name'],
|
$oldUser['user_email'],
|
$newUser['user_email'],
|
$this->divider(),
|
$this->button(wp_login_url(), 'Log In To Your Account')
|
);
|
$content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser);
|
$email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed');
|
$prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true;
|
$prefix = ($prefix) ? $this->prefix : '';
|
$email_change_email['subject'] = $prefix.JVB_EMAIL['types']['emailChange']['subject']?:'Email Address Changed';
|
|
|
return $email_change_email;
|
}
|
|
/**
|
* New email address confirmation
|
* @param string $email_text
|
* @param array $user
|
*
|
* @return string
|
*/
|
public function customizeNewUserEmailContent(string $email_text, array $new_user_email):string
|
{
|
$confirm_url = esc_url( add_query_arg('newuseremail', $new_user_email['hash'], self_admin_url('profile.php')));
|
$content = sprintf(
|
'<p>Hey,</p>
|
<p>There was a request to change the email address associated with your account to this one.</p>
|
<p>This is just a friendly email to ensure you would like this change.</p>
|
<p>You can confirm this change by clicking the button below:</p>
|
%s
|
<p>Or copy and paste this link into your browser:</p>
|
%s',
|
$this->button($confirm_url, 'Confirm this Email'),
|
$this->link($confirm_url)
|
);
|
|
$content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url);
|
|
$content .= $this->divider();
|
$content .= '<p>If you did not request this change, you can safely ignore this email and nothing will change.</p>';
|
|
return $this->getEmailTemplate($content, 'Confirm Email Change');
|
}
|
|
/**
|
* Password change notification
|
* @param array $pass_change_email
|
* @param array $oldUser
|
* @param array $newUser
|
*
|
* @return array
|
*/
|
public function customizePasswordChangeEmail(array $pass_change_email, array $oldUser, array $newUser):array
|
{
|
$content = sprintf(
|
'<p>Hi %s,</p>
|
<p>This is a confirmation email to let you know your password has successfully been changed.</p>
|
<p>If you\'re not expecting this email, and did not change your password - please <strong>contact us immediately</strong></p>
|
<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>
|
%s',
|
$oldUser['first_name'],
|
$this->button(wp_login_url(), 'Log In to Your Account')
|
);
|
$content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser);
|
|
$pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed');
|
$prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true;
|
$prefix = ($prefix) ? $this->prefix : '';
|
$pass_change_email['subject'] = $prefix.JVB_EMAIL['types']['passwordChange']['subject']?:'Password Changed';
|
|
return $pass_change_email;
|
}
|
|
/**
|
* User data request confirmation email
|
* @param string $content
|
* @param array $request_data
|
*
|
* @return string
|
*/
|
public function customizeUserRequestEmail(string $content, array $email_data):string
|
{
|
|
$confirm_url = $email_data['confirm_url'];
|
$request_type = $email_data['action_name'];
|
|
switch ($request_type) {
|
case 'export_personal_data':
|
$request_name = 'Export Personal Data';
|
break;
|
case 'remove_personal_data':
|
$request_name = 'Erase Personal Data';
|
break;
|
default:
|
$request_name = 'Data Request';
|
}
|
|
|
$message = sprintf(
|
'<p>Hi,</p>
|
<p>You\'re receiving this email because a request has been made to <strong>%s</strong></p>
|
<p>If you\'re the one who made this request, you can confirm it by clicking the button below:</p>
|
%s
|
<p>Or copy and paste this link into your browser:</p>
|
%s',
|
$request_name,
|
$this->button($confirm_url, 'Confirm'),
|
$this->link($confirm_url)
|
);
|
$message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data);
|
|
$message .= $this->divider();
|
$message .= '<p>If you did not make this request, you can safely ignore this email.</p>';
|
|
return $this->getEmailTemplate($message, 'Action Confirmation');
|
}
|
|
/**
|
* Personal data export email
|
* @param string $content
|
* @param int $request_id
|
* @param array $email_data
|
*
|
* @return string
|
*/
|
public function customizePersonalDataEmail(string $content, int $request_id, array $email_data):string
|
{
|
$download_url = $email_data['export_file_url'];
|
$expiresAt = $email_data['expiration_date'];
|
|
$message = sprintf(
|
'<p>Hi,</p>
|
<p>You\'re receiving this email because you requested an export of your personal data.</p>
|
<p>You can download your personal data by clicking the button below:</p>
|
%s
|
<p>Or you can copy and paste this link into your browser:</p>
|
%s
|
%s
|
<p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>',
|
$this->button($download_url, 'Download Your Data'),
|
$this->link($download_url),
|
$this->divider(),
|
$expiresAt
|
);
|
$message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data);
|
|
return $this->getEmailTemplate($message, 'Your Personal Data Export');
|
}
|
|
public function signature():string
|
{
|
return '<p><i>'.$this->signature.'</i></p>';
|
}
|
|
public function button(string $link, string $title):string
|
{
|
return sprintf(
|
'<p style="text-align: center;"><a href="%s" style="display:inline-block;padding:16px 10px;border-radius:4px;background-color:%s;color:%s;text-decoration:none;font-weight:bold;margin:15px 0;letter-spacing:1px;text-transform:uppercase;">%s</a></p>',
|
$link,
|
$this->colours['action-0'],
|
$this->colours['action-contrast'],
|
$title
|
);
|
}
|
|
public function link(string $link):string
|
{
|
return sprintf(
|
'<code style="color:%s;border:1px solid %s;background-color:%s;border-radius:4px;user-select:all;">%s</code>',
|
$this->colours['dark-200'],
|
$this->colours['dark-100'],
|
$this->colours['light-100'],
|
$link
|
);
|
}
|
|
public function divider():string
|
{
|
return '<div style="border-top:1px solid '.$this->colours['dark-200'].';margin:25px 0;"></div>';
|
}
|
|
public function notice(string $text):string
|
{
|
return '<div style="border-radius: 0 8px 8px 0; margin: 1rem 0; border-left: 4px solid '.$this->colours['action-0'].';padding:1rem;background-color:'.$this->colours['light-100'].';">
|
'.str_replace('<strong>', '<strong style="color:'.$this->colours['action-0'].'">',$text).'
|
</div>';
|
}
|
|
public function callout(string $text):string
|
{
|
return sprintf(
|
'<div style="padding:2rem;margin:2rem 3rem;background-color:%s;color:%s;">%s</div>',
|
$this->colours['action-0'],
|
$this->colours['action-contrast'],
|
str_replace('<a', '<a style="background-color:'.$this->colours['action-contrast'].';padding: 0 .125rem;border-radius:4px;"', $text)
|
);
|
}
|
|
public function table(array $summarize, string $title = '', array $actions = []):string
|
{
|
if (empty($summarize)){
|
return '';
|
}
|
|
if (!empty($title)) {
|
$title = sprintf(
|
'<h2 style="color:%s;border-bottom:2px solid %s;padding-bottom:15px;margin-bottom:20px;text-align:center;">%s</h2>',
|
$this->colours['dark-200'],
|
$this->colours['dark-200'],
|
$title
|
);
|
}
|
$content = '';
|
foreach ($summarize as $index=> $item) {
|
if (!array_key_exists('label', $item) && !array_key_exists('value', $item)) {
|
continue;
|
}
|
$content .= sprintf(
|
'<div style="padding:10px 0;border-bottom:1px solid %s;background-color:%s;">
|
<span style="display:inline-block;vertical-align:top;font-weight:600;color:%s;width:%s;">%s</span>
|
<div style="display:inline-block;vertical-align:top;width:%s;">%s</div>
|
</div>',
|
$this->colours['dark-200'],
|
($index%2 === 0) ? $this->colours['light-100'] : $this->colours['light-50'],
|
$this->colours['dark-200'],
|
'30%', // Changed from 19% to 30%
|
$item['label'],
|
'68%', // Changed from 80% to 68% (30% + 68% = 98%, leaving 2% for spacing)
|
$item['value']
|
);
|
}
|
|
return sprintf(
|
'<div style="max-width:500px;margin:40px auto;padding:30px;background-color:%s;border-radius:10px;border:2px dashed %s;">%s%s</div>',
|
$this->colours['light-100'],
|
$this->colours['dark-200'],
|
$title,
|
$content
|
);
|
}
|
|
public function card(string $content, string $title = ''):string
|
{
|
$titleHtml = '';
|
if (!empty($title)) {
|
$titleHtml = sprintf(
|
'<h3 style="margin:0 0 15px 0;color:%s;font-size:18px;font-weight:600;">%s</h3>',
|
$this->colours['dark'],
|
esc_html($title)
|
);
|
}
|
|
return sprintf(
|
'<div style="background-color:%s;border:1px solid %s;border-radius:8px;padding:20px;margin:15px 0;">%s%s</div>',
|
$this->colours['light-50'],
|
$this->colours['light-200'],
|
$titleHtml,
|
$content
|
);
|
}
|
|
public function stat(string $number, string $label, string $description = ''):string
|
{
|
$desc = $description ? sprintf(
|
'<p style="margin:5px 0 0 0;font-size:12px;color:%s;">%s</p>',
|
$this->colours['dark-200'],
|
esc_html($description)
|
) : '';
|
|
return sprintf(
|
'<div style="text-align:center;padding:20px;background-color:%s;border-radius:8px;margin:10px 0;">
|
<div style="font-size:36px;font-weight:700;color:%s;margin:0 0 5px 0;">%s</div>
|
<div style="font-size:14px;font-weight:600;color:%s;text-transform:uppercase;letter-spacing:1px;">%s</div>
|
%s
|
</div>',
|
$this->colours['light-100'],
|
$this->colours['action-0'],
|
esc_html($number),
|
$this->colours['dark-200'],
|
esc_html($label),
|
$desc
|
);
|
}
|
|
public function grid(array $items, int $columns = 2, string $title = '', string|array $description = '', string $after = ''):string
|
{
|
$width = floor(100 / $columns) - 2; // 2% gap
|
|
$html = '';
|
if (!empty($title) || !empty($description)) {
|
$html .= '<div>';
|
if (!empty($title)) {
|
$html .= sprintf(
|
'<h2>%s</h2>',
|
$title
|
);
|
}
|
if (!empty($description)) {
|
if (is_string($description)) {
|
if (str_starts_with($description, '<p>')) {
|
$html .= $description;
|
}else {
|
$html .= sprintf(
|
'<p>%s</p>',
|
$description
|
);
|
}
|
} else {
|
$html .= implode('',array_map(function ($p) { return sprintf('<p>%s</p>', $p); }, $description));
|
}
|
}
|
}
|
$html .= '<div style="display:table;width:100%;margin:20px 0;">';
|
foreach ($items as $index => $item) {
|
if ($index > 0 && $index % $columns === 0) {
|
$html .= '</div><div style="display:table;width:100%;margin:20px 0;">';
|
}
|
$html .= sprintf(
|
'<div style="display:table-cell;width:%d%%;padding:10px;vertical-align:top;">%s</div>',
|
$width,
|
$item
|
);
|
}
|
$html .= '</div>'.$after;
|
|
|
if (!empty($title) || !empty($description)) {
|
$html .= '</div>';
|
}
|
return $html;
|
}
|
|
|
public function image(string $src, string $alt = '', int $maxWidth = 600):string
|
{
|
return sprintf(
|
'<div style="text-align:center;margin:20px 0;">
|
<img src="%s" alt="%s" style="max-width:%dpx;width:100%%;height:auto;border-radius:8px;" />
|
</div>',
|
esc_url($src),
|
esc_attr($alt),
|
$maxWidth
|
);
|
}
|
|
public function h1(string $text):string
|
{
|
return sprintf(
|
'<h1 style="color:%s;font-size:24px;font-weight:700;margin:20px 0 10px 0;">%s</h1>',
|
$this->colours['dark'],
|
$text
|
);
|
}
|
|
public function h2(string $text):string
|
{
|
return sprintf(
|
'<h2 style="color:%s;font-size:20px;font-weight:600;margin:20px 0 10px 0;border-bottom:2px solid %s;padding-bottom:10px;">%s</h2>',
|
$this->colours['dark'],
|
$this->colours['dark-200'],
|
$text
|
);
|
}
|
|
public function h3(string $text):string
|
{
|
return sprintf(
|
'<h3 style="color:%s;font-size:18px;font-weight:600;margin:15px 0 10px 0;">%s</h3>',
|
$this->colours['dark-200'],
|
$text
|
);
|
}
|
|
public function list(array $items, bool $ordered = false):string
|
{
|
$tag = $ordered ? 'ol' : 'ul';
|
$style = $ordered
|
? 'list-style-type:decimal;padding-left:20px;margin:10px 0;'
|
: 'list-style-type:disc;padding-left:20px;margin:10px 0;';
|
|
$html = sprintf('<%s style="%s">', $tag, $style);
|
foreach ($items as $item) {
|
$html .= sprintf(
|
'<li style="margin:5px 0;line-height:1.6;">%s</li>',
|
$item
|
);
|
}
|
$html .= sprintf('</%s>', $tag);
|
|
return $html;
|
}
|
|
public function codeBlock(string $code, string $language = ''):string
|
{
|
return sprintf(
|
'<pre style="background-color:%s;border:1px solid %s;border-radius:4px;padding:15px;overflow-x:auto;font-family:\'Courier New\',monospace;font-size:13px;line-height:1.4;"><code>%s</code></pre>',
|
$this->colours['light-100'],
|
$this->colours['dark-200'],
|
esc_html($code)
|
);
|
}
|
|
public function badge(string $text, string $type = 'default'):string
|
{
|
$colors = [
|
'success' => ['bg' => '#d4edda', 'text' => '#155724'],
|
'warning' => ['bg' => '#fff3cd', 'text' => '#856404'],
|
'error' => ['bg' => '#f8d7da', 'text' => '#721c24'],
|
'info' => ['bg' => '#d1ecf1', 'text' => '#0c5460'],
|
'default' => ['bg' => $this->colours['light-200'], 'text' => $this->colours['dark-200']]
|
];
|
|
$color = $colors[$type] ?? $colors['default'];
|
|
return sprintf(
|
'<span style="display:inline-block;padding:4px 12px;border-radius:12px;background-color:%s;color:%s;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">%s</span>',
|
$color['bg'],
|
$color['text'],
|
esc_html($text)
|
);
|
}
|
|
public function alert(string $text, string $type = 'info'):string
|
{
|
$configs = [
|
'success' => ['bg' => '#d4edda', 'border' => '#28a745', 'text' => '#155724', 'icon' => '✓'],
|
'warning' => ['bg' => '#fff3cd', 'border' => '#ffc107', 'text' => '#856404', 'icon' => '⚠'],
|
'error' => ['bg' => '#f8d7da', 'border' => '#dc3545', 'text' => '#721c24', 'icon' => '✕'],
|
'info' => ['bg' => '#d1ecf1', 'border' => $this->colours['action-0'], 'text' => '#0c5460', 'icon' => 'ℹ']
|
];
|
|
$config = $configs[$type] ?? $configs['info'];
|
|
return sprintf(
|
'<div style="border-radius:8px;margin:1rem 0;border-left:4px solid %s;padding:1rem;background-color:%s;color:%s;">
|
<strong style="font-size:16px;">%s</strong> %s
|
</div>',
|
$config['border'],
|
$config['bg'],
|
$config['text'],
|
$config['icon'],
|
$text
|
);
|
}
|
|
public function spacer(int $height = 20):string
|
{
|
return sprintf('<div style="height:%dpx;"></div>', $height);
|
}
|
|
public function prefix():string
|
{
|
return $this->prefix;
|
}
|
}
|