From b0d1a0ecbdad2544d629522528a1b879524cf686 Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Tue, 12 May 2026 00:42:26 +0200 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20v1.1=20beta=20=E2=80=94=20login=20s?= =?UTF-8?q?tyling=20settings=20+=20iframe=20defenses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- includes/Admin/Settings.php | 110 ++++++++++++++++ includes/Frontend/LoginScreen.php | 5 + includes/Installer.php | 4 + includes/helpers.php | 4 + readme.txt | 4 + templates/login-shell.php | 28 ++++- tests/phpunit/Model/SettingsSanitizeTest.php | 124 +++++++++++++++++++ tests/phpunit/stubs/wp-functions.php | 27 ++++ 8 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 tests/phpunit/Model/SettingsSanitizeTest.php diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 8a8d95a..9c4b19e 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -28,6 +28,8 @@ final class Settings { private const MAX_LINK_USES_PRESETS = [ 1, 2, 3, 5, 10 ]; + private const FONT_STACK_KEYS = [ 'system', 'sans-modern', 'serif', 'mono', 'rounded' ]; + // Keyed by extension regex (wp_check_filetype_and_ext format); membership checks hit values. private const LOGO_MIMES = [ 'png' => 'image/png', @@ -199,6 +201,10 @@ private static function render_section_branding( bool $weak_salts ): void { + + + + +
+
+ +

+
+
+
+ + +
+
+
+ +
+
+ +

+
+
+ + +
+
+ +
+
+ +

+
+
+ + +
+
+ __( 'System default', 'magicauth' ), + 'sans-modern' => __( 'Modern sans (Inter)', 'magicauth' ), + 'serif' => __( 'Serif (Georgia)', 'magicauth' ), + 'mono' => __( 'Monospace', 'magicauth' ), + 'rounded' => __( 'Rounded', 'magicauth' ), + ]; + ?> +
+
+ +

+
+
+
+ +
+
+
+ '', 'logo_attachment_id' => 0, 'brand_color' => '#2271b1', + 'page_color' => '#eeeeee', + 'card_radius' => 6, + 'card_width' => 480, + 'font_stack' => 'system', 'agency_credit_name' => '', 'agency_credit_url' => '', 'agency_credit_icon_id' => 0, diff --git a/includes/helpers.php b/includes/helpers.php index e328bbc..4d2983d 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -30,6 +30,10 @@ function magicauth_get_settings(): array { 'company_name' => '', 'logo_attachment_id' => 0, 'brand_color' => '#2271b1', + 'page_color' => '#eeeeee', + 'card_radius' => 6, + 'card_width' => 480, + 'font_stack' => 'system', 'agency_credit_name' => '', 'agency_credit_url' => '', 'agency_credit_icon_id' => 0, diff --git a/readme.txt b/readme.txt index 9914330..0a15db5 100644 --- a/readme.txt +++ b/readme.txt @@ -74,5 +74,9 @@ MagicAuth registers a WordPress privacy exporter and eraser. Personal data store == Changelog == += 1.1.0 = +* Branding: page background color, card corner radius, card max width, and font family are now admin-controllable from Settings > MagicAuth. +* Security: `wp-login.php` now sends `X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'none'` to refuse framing entirely. + = 1.0.0 = * Initial public release. diff --git a/templates/login-shell.php b/templates/login-shell.php index a1f118d..fa6ff76 100644 --- a/templates/login-shell.php +++ b/templates/login-shell.php @@ -19,17 +19,43 @@ defined( 'ABSPATH' ) || exit; ?> ' or a future maintainer can break the ' as a block-terminator regardless of CSS + // quoting. Pin a static assertion so a future maintainer adding e.g. + // 'fancy' => 'Helvetica' cannot silently break the assertEqualsWithDelta( 21.0, magicauth_contrast_ratio( '#000', '#ffffff' ), 0.01 ); + } + + public function test_identical_colors_yield_1(): void { + $this->assertEqualsWithDelta( 1.0, magicauth_contrast_ratio( '#abcdef', '#abcdef' ), 0.001 ); + } + + public function test_wp_blue_on_white_is_aa_normal(): void { + $r = magicauth_contrast_ratio( '#2271b1', '#ffffff' ); + $this->assertGreaterThanOrEqual( 4.5, $r ); + $this->assertLessThan( 7.0, $r ); + } + + public function test_three_digit_hex_normalizes_like_six(): void { + $this->assertEqualsWithDelta( + magicauth_contrast_ratio( '#abc', '#ffffff' ), + magicauth_contrast_ratio( '#aabbcc', '#ffffff' ), + 0.01 + ); + } + + public function test_symmetric(): void { + $a = magicauth_contrast_ratio( '#123456', '#abcdef' ); + $b = magicauth_contrast_ratio( '#abcdef', '#123456' ); + $this->assertEqualsWithDelta( $a, $b, 0.001 ); + } + + public function test_garbage_input_returns_1(): void { + $this->assertSame( 1.0, magicauth_contrast_ratio( 'pirate', '#ffffff' ) ); + } + + public function test_alpha_hex_rejected(): void { + // 8-char form is not a valid 3/6-digit hex → 1.0 sentinel. + $this->assertSame( 1.0, magicauth_contrast_ratio( '#ff000080', '#ffffff' ) ); + } + + public function test_evaluate_thresholds(): void { + $this->assertSame( 'fail', magicauth_contrast_evaluate( 2.0, 'normal' ) ); + $this->assertSame( 'warn', magicauth_contrast_evaluate( 4.0, 'normal' ) ); + $this->assertSame( 'pass', magicauth_contrast_evaluate( 5.0, 'normal' ) ); + $this->assertSame( 'pass', magicauth_contrast_evaluate( 3.5, 'ui' ) ); + $this->assertSame( 'pass', magicauth_contrast_evaluate( 3.5, 'large' ) ); + } +} diff --git a/tests/phpunit/Model/SettingsSanitizeTest.php b/tests/phpunit/Model/SettingsSanitizeTest.php index 539861c..1a399bd 100644 --- a/tests/phpunit/Model/SettingsSanitizeTest.php +++ b/tests/phpunit/Model/SettingsSanitizeTest.php @@ -102,6 +102,151 @@ public function test_defaults_returned_for_missing_keys(): void { $this->assertSame( 'system', $settings['font_stack'] ); } + public function test_link_color_accepts_empty(): void { + $out = $this->sanitize( [ 'link_color' => '' ] ); + $this->assertSame( '', $out['link_color'] ); + $this->assertEmpty( $this->settings_error_codes(), 'empty link_color should produce no notice' ); + } + + public function test_link_color_accepts_high_contrast_hex(): void { + $out = $this->sanitize( [ 'link_color' => '#0044aa' ] ); + $this->assertSame( '#0044aa', $out['link_color'] ); + $this->assertEmpty( $this->settings_error_codes(), 'AA+ link_color should be silent' ); + } + + public function test_link_color_rejects_invalid_hex(): void { + $out = $this->sanitize( [ 'link_color' => 'pirate' ] ); + // Default link_color is empty; invalid input restores previous value. + $this->assertSame( '', $out['link_color'] ); + $this->assertContains( 'magicauth_link_color_invalid', $this->settings_error_codes() ); + } + + public function test_link_color_rejects_unreadable_below_floor(): void { + // #bbbbbb on #ffffff ≈ 1.9:1 — well under the 2.5 hard floor. + $out = $this->sanitize( [ 'link_color' => '#bbbbbb' ] ); + $this->assertSame( '', $out['link_color'], 'fail verdict must restore previous (empty default)' ); + $this->assertContains( 'magicauth_link_color_unreadable', $this->settings_error_codes() ); + } + + public function test_link_color_saves_with_warning_in_warn_band(): void { + // #888888 on #ffffff ≈ 3.5:1 — between the 2.5 floor and AA 4.5. + $out = $this->sanitize( [ 'link_color' => '#888888' ] ); + $this->assertSame( '#888888', $out['link_color'], 'warn verdict must still save' ); + $this->assertContains( 'magicauth_link_color_low_contrast', $this->settings_error_codes() ); + } + + public function test_link_color_array_input_is_ignored(): void { + $out = $this->sanitize( [ 'link_color' => [ '#ff0000' ] ] ); + $this->assertSame( '', $out['link_color'] ); + } + + public function test_color_mode_round_trips_allowlist(): void { + foreach ( [ 'light', 'dark', 'auto' ] as $mode ) { + $out = $this->sanitize( [ 'color_mode' => $mode ] ); + $this->assertSame( $mode, $out['color_mode'], "mode '{$mode}' should round-trip" ); + } + } + + public function test_color_mode_rejects_unknown(): void { + $out = $this->sanitize( [ 'color_mode' => 'midnight' ] ); + $this->assertSame( 'light', $out['color_mode'] ); + } + + public function test_color_mode_rejects_array_input(): void { + $out = $this->sanitize( [ 'color_mode' => [ 'dark' ] ] ); + // Default survives via is_scalar guard. + $this->assertSame( 'light', $out['color_mode'] ); + } + + public function test_brand_dark_contrast_warning_fires_for_low_contrast(): void { + $out = $this->sanitize( [ + 'brand_color' => '#444444', + 'color_mode' => 'dark', + ] ); + $this->assertSame( '#444444', $out['brand_color'] ); + $this->assertSame( 'dark', $out['color_mode'] ); + $this->assertContains( 'magicauth_brand_dark_contrast', $this->settings_error_codes() ); + } + + public function test_brand_dark_contrast_silent_in_light_mode(): void { + // Same low-contrast brand, but light mode — no dark-surface check applies. + $out = $this->sanitize( [ + 'brand_color' => '#444444', + 'color_mode' => 'light', + ] ); + $this->assertNotContains( 'magicauth_brand_dark_contrast', $this->settings_error_codes() ); + } + + public function test_background_id_zero_clears(): void { + $out = $this->sanitize( [ 'background_attachment_id' => 0 ] ); + $this->assertSame( 0, $out['background_attachment_id'] ); + $this->assertEmpty( $this->settings_error_codes(), 'zero should be silent' ); + } + + public function test_background_id_rejects_non_image_attachment(): void { + $path = $this->make_temp_file( '.png', $this->one_pixel_png_bytes() ); + try { + magicauth_test_register_attachment_file( 42, $path, false /* is_image = false */ ); + $out = $this->sanitize( [ 'background_attachment_id' => 42 ] ); + $this->assertSame( 0, $out['background_attachment_id'] ); + $this->assertContains( 'magicauth_bg_not_image', $this->settings_error_codes() ); + } finally { + @unlink( $path ); + } + } + + public function test_background_id_accepts_valid_png(): void { + $path = $this->make_temp_file( '.png', $this->one_pixel_png_bytes() ); + try { + magicauth_test_register_attachment_file( 7, $path, true ); + $out = $this->sanitize( [ 'background_attachment_id' => 7 ] ); + $this->assertSame( 7, $out['background_attachment_id'] ); + $this->assertEmpty( $this->settings_error_codes(), 'valid PNG should be silent' ); + } finally { + @unlink( $path ); + } + } + + public function test_background_id_rejects_svg(): void { + $path = $this->make_temp_file( '.svg', '' ); + try { + magicauth_test_register_attachment_file( 11, $path, true ); + $out = $this->sanitize( [ 'background_attachment_id' => 11 ] ); + $this->assertSame( 0, $out['background_attachment_id'] ); + $this->assertContains( 'magicauth_bg_bad_ext', $this->settings_error_codes() ); + } finally { + @unlink( $path ); + } + } + + public function test_background_id_returns_zero_when_path_missing(): void { + // Image flag set but no path registered → get_attached_file() returns false. + global $magicauth_test_state; + $magicauth_test_state['attachment_is_image'][ 99 ] = true; + $out = $this->sanitize( [ 'background_attachment_id' => 99 ] ); + $this->assertSame( 0, $out['background_attachment_id'] ); + } + + private function make_temp_file( string $extension, string $bytes ): string { + $path = sys_get_temp_dir() . '/magicauth-bg-test-' . uniqid( '', true ) . $extension; + file_put_contents( $path, $bytes ); + return $path; + } + + private function one_pixel_png_bytes(): string { + // Minimal 1×1 transparent PNG — finfo identifies as image/png. + return base64_decode( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', + true + ) ?: ''; + } + + /** @return list */ + private function settings_error_codes(): array { + $errors = $GLOBALS['magicauth_test_state']['settings_errors'] ?? []; + return array_map( static fn ( $e ) => (string) ( $e['code'] ?? '' ), $errors ); + } + public function test_font_stacks_in_shell_template_contain_no_html_meta_chars(): void { // HTML parsers treat '' as a block-terminator regardless of CSS // quoting. Pin a static assertion so a future maintainer adding e.g. diff --git a/tests/phpunit/stubs/wp-functions.php b/tests/phpunit/stubs/wp-functions.php index ace9f1a..94208cd 100644 --- a/tests/phpunit/stubs/wp-functions.php +++ b/tests/phpunit/stubs/wp-functions.php @@ -48,6 +48,9 @@ function update_option( string $option, $value, $autoload = null ): bool { unset( $autoload ); global $magicauth_test_state; $magicauth_test_state['options'][ $option ] = $value; + if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) { + magicauth_invalidate_settings_cache(); + } return true; } } @@ -59,6 +62,9 @@ function add_option( string $option, $value ): bool { return false; } $magicauth_test_state['options'][ $option ] = $value; + if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) { + magicauth_invalidate_settings_cache(); + } return true; } } @@ -67,6 +73,9 @@ function add_option( string $option, $value ): bool { function delete_option( string $option ): bool { global $magicauth_test_state; unset( $magicauth_test_state['options'][ $option ] ); + if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) { + magicauth_invalidate_settings_cache(); + } return true; } } @@ -656,6 +665,37 @@ function wp_get_attachment_image_src( int $id, string $size = 'thumbnail' ) { } } +if ( ! function_exists( 'wp_attachment_is_image' ) ) { + function wp_attachment_is_image( int $id ): bool { + global $magicauth_test_state; + return ! empty( $magicauth_test_state['attachment_is_image'][ $id ] ); + } +} + +if ( ! function_exists( 'get_attached_file' ) ) { + function get_attached_file( int $id ) { + global $magicauth_test_state; + return $magicauth_test_state['attachment_paths'][ $id ] ?? false; + } +} + +if ( ! function_exists( 'wp_check_filetype_and_ext' ) ) { + // Simplified: match by file extension against the allowlist regex keys. + // Real WP also peeks at content via finfo; we leave that to the caller's + // own finfo block, since duplicating WP's logic here adds little value. + function wp_check_filetype_and_ext( string $file, string $filename, array $mimes = [] ): array { + unset( $file ); + $ext = strtolower( (string) pathinfo( $filename, PATHINFO_EXTENSION ) ); + foreach ( $mimes as $regex => $mime ) { + $alternatives = explode( '|', $regex ); + if ( in_array( $ext, $alternatives, true ) ) { + return [ 'ext' => $ext, 'type' => $mime, 'proper_filename' => false ]; + } + } + return [ 'ext' => false, 'type' => false, 'proper_filename' => false ]; + } +} + if ( ! function_exists( 'magicauth_test_register_attachment' ) ) { function magicauth_test_register_attachment( int $id, string $url ): void { global $magicauth_test_state; @@ -663,6 +703,19 @@ function magicauth_test_register_attachment( int $id, string $url ): void { } } +if ( ! function_exists( 'magicauth_test_register_attachment_file' ) ) { + /** + * Test helper: bind an attachment ID to an on-disk path so sanitize_background() + * sees a real file (finfo needs bytes). Marks the attachment as an image by + * default; pass $is_image=false to simulate a non-image attachment. + */ + function magicauth_test_register_attachment_file( int $id, string $path, bool $is_image = true ): void { + global $magicauth_test_state; + $magicauth_test_state['attachment_paths'][ $id ] = $path; + $magicauth_test_state['attachment_is_image'][ $id ] = $is_image; + } +} + if ( ! function_exists( 'magicauth_test_register_user' ) ) { /** * Test helper: register a stub WP_User in the in-memory user table. @@ -729,14 +782,19 @@ function add_settings_error( string $setting, string $code, string $message, str function magicauth_test_reset_state(): void { global $magicauth_test_state, $wpdb; $magicauth_test_state = [ - 'options' => [], - 'usermeta' => [], - 'transients' => [], - 'users' => [], - 'actions' => [], - 'filters' => [], - 'attachments' => [], + 'options' => [], + 'usermeta' => [], + 'transients' => [], + 'users' => [], + 'actions' => [], + 'filters' => [], + 'attachments' => [], + 'attachment_paths' => [], + 'attachment_is_image' => [], ]; + if ( function_exists( 'magicauth_invalidate_settings_cache' ) ) { + magicauth_invalidate_settings_cache(); + } if ( isset( $wpdb ) && method_exists( $wpdb, 'truncate_magicauth_table' ) ) { $wpdb->truncate_magicauth_table(); } From f21d9ed19f787600764dac6788a63da140d226b8 Mon Sep 17 00:00:00 2001 From: r00bbert <280805750+r00bbert@users.noreply.github.com> Date: Tue, 12 May 2026 14:11:45 +0200 Subject: [PATCH 3/7] fix: split branding and appearance --- assets/css/magicauth-admin.css | 14 -------------- includes/Admin/Settings.php | 14 +++++++++++--- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/assets/css/magicauth-admin.css b/assets/css/magicauth-admin.css index 36db28f..242fc93 100644 --- a/assets/css/magicauth-admin.css +++ b/assets/css/magicauth-admin.css @@ -413,20 +413,6 @@ body.admin-bar .magicauth-admin .magicauth-topbar__bar { top: 32px; } border-radius: 4px; } -/* Sub-card subheading inside .magicauth-card */ -.magicauth-admin .magicauth-card__subhead { - margin: 0 0 16px; - font-size: 13px; - font-weight: 600; - color: var(--tx-muted); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.magicauth-admin .magicauth-card + .magicauth-card { - margin-top: 16px; -} - /* Cards. Namespaced to avoid colliding with WP core `.card` (caps at 520px). */ .magicauth-admin .magicauth-card { background: var(--ettic-surface); diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 47691fa..b1bbd08 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -178,6 +178,7 @@ public static function render_page(): void {
+ @@ -211,18 +212,25 @@ private static function render_section_branding( bool $weak_salts ): void {

-
-

+ + +
+ +

+

+
-

From 5ec6e8fe8c2f1254c97c4989d5c5e0bb6aee266a Mon Sep 17 00:00:00 2001 From: r00bbert <280805750+r00bbert@users.noreply.github.com> Date: Tue, 12 May 2026 14:12:38 +0200 Subject: [PATCH 4/7] fix: link color follows brand when blank --- assets/js/magicauth-admin.js | 44 ++++++++++++++++++++++++++++++++++++ includes/Admin/Settings.php | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/assets/js/magicauth-admin.js b/assets/js/magicauth-admin.js index 6424c47..67b59ce 100644 --- a/assets/js/magicauth-admin.js +++ b/assets/js/magicauth-admin.js @@ -12,6 +12,7 @@ function init() { initMediaPickers(); initColorPickers(); + initColorFollowers(); initRowDirtyMarks(); initDirtyTracking(); initCharCounters(); @@ -107,6 +108,49 @@ } ); } + // Follower color pickers: when the text input is blank, the swatch mirrors + // a source picker (e.g. link_color follows brand_color). Typing a hex or + // picking a swatch color writes to the text input, which disconnects. + function initColorFollowers() { + var HEX_RE = /^#?[0-9a-fA-F]{6}$/; + var normalize = function ( v ) { return v.charAt( 0 ) === '#' ? v : '#' + v; }; + + document.querySelectorAll( '.magicauth-admin .magicauth-color[data-magicauth-color-follows]' ).forEach( function ( follower ) { + var sourceKey = follower.getAttribute( 'data-magicauth-color-follows' ); + if ( ! sourceKey ) { + return; + } + var sourceText = document.querySelector( 'input[name="magicauth_settings[' + sourceKey + ']"]' ); + var swatch = follower.querySelector( 'input[type="color"]' ); + var text = follower.querySelector( 'input[type="text"]' ); + if ( ! sourceText || ! swatch || ! text ) { + return; + } + + function isConnected() { return '' === text.value.trim(); } + function mirrorFromSource() { + var v = sourceText.value.trim(); + if ( HEX_RE.test( v ) ) { + swatch.value = normalize( v ); + } + } + + // Cleared text → snap swatch back to source. + text.addEventListener( 'input', function () { + if ( isConnected() ) { + mirrorFromSource(); + } + } ); + + // Source moved → mirror only while connected. + sourceText.addEventListener( 'input', function () { + if ( isConnected() ) { + mirrorFromSource(); + } + } ); + } ); + } + // Per-row dirty mark — auto-injected, opacity toggled via .is-dirty function initRowDirtyMarks() { document.querySelectorAll( '.magicauth-admin .magicauth-row' ).forEach( function ( row ) { diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index 47691fa..f29015d 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -870,7 +870,7 @@ public static function field_link_color(): void {

-
+
From ad87fbc62d52f571274a466cbc09c3727f86ba80 Mon Sep 17 00:00:00 2001 From: r00bbert <280805750+r00bbert@users.noreply.github.com> Date: Tue, 12 May 2026 14:13:11 +0200 Subject: [PATCH 5/7] fix: match diagnostics row padding --- assets/css/magicauth-admin.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/magicauth-admin.css b/assets/css/magicauth-admin.css index 36db28f..c5b676f 100644 --- a/assets/css/magicauth-admin.css +++ b/assets/css/magicauth-admin.css @@ -885,7 +885,7 @@ body.admin-bar .magicauth-admin .magicauth-topbar__bar { top: 32px; } align-items: center; justify-content: space-between; gap: 16px; - padding: 14px 8px; + padding: 14px; position: relative; } From 124fdc189e86c0a317ddcd4b1a85daa31d53ae97 Mon Sep 17 00:00:00 2001 From: Nol de Roos <108540791+nolderoos@users.noreply.github.com> Date: Tue, 12 May 2026 14:59:33 +0200 Subject: [PATCH 6/7] fix: point Plugin URI to plugins.ettic.nl/magicauth Updates the Plugin URI header so the "Visit plugin site" link on the WordPress Plugins screen resolves to the canonical product page instead of the GitHub source repo. --- magicauth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicauth.php b/magicauth.php index 21db5cb..401a33f 100644 --- a/magicauth.php +++ b/magicauth.php @@ -1,7 +1,7 @@ Date: Wed, 13 May 2026 11:56:15 +0200 Subject: [PATCH 7/7] fix: address CodeRabbit feedback on login styling PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Item 4 (Minor): initialize follower swatch on first render so the preview matches the source picker before any user input. https://github.com/EtticDevelopment/magicauth/pull/4#discussion_r3226398090 - Item 5 (Nitpick): add prominent bidirectional SYNC-LOCK comments cross-referencing the duplicated dark-surface hex in both assets/css/magicauth.css and includes/Admin/Settings.php. Chose option (c) — option (a) would require injecting only one of nine dark-mode tokens via PHP, creating asymmetry vs the eight others that legitimately live in CSS. - Item 6 (Nitpick): same SYNC-LOCK comment on the PHP constant side names both CSS line numbers and the cascade direction. - Item 7 (Nitpick): extract magic number into private const BACKGROUND_SIZE_SOFT_LIMIT_BYTES = 1024 * 1024 and replace both call sites. Co-Authored-By: Claude Opus 4.7 (1M context) --- assets/css/magicauth.css | 18 ++++++++++++++---- assets/js/magicauth-admin.js | 5 +++++ includes/Admin/Settings.php | 18 +++++++++++++----- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/assets/css/magicauth.css b/assets/css/magicauth.css index dd22b26..a93abd0 100644 --- a/assets/css/magicauth.css +++ b/assets/css/magicauth.css @@ -451,10 +451,16 @@ /* Dark mode — explicit. Body-class specificity (0,1,1) beats :root (0,0,1), so the shell-vars block can still set per-instance brand_color / link_color / font_stack without being overridden. Curated values, not exposed to admin - pickers. The surface hex must match Admin\Settings::DARK_SURFACE_HEX. */ + pickers. + !!! SYNC-LOCK !!! --magicauth-color-surface (#181c22) MUST equal the PHP + constant MagicAuth\Admin\Settings::DARK_SURFACE_HEX + (includes/Admin/Settings.php:~42). The PHP constant feeds the brand-color + contrast check; the CSS variable paints the card. If either changes without + the other, dark-mode contrast warnings will lie. Value also duplicated at + line ~482 below (auto-mode @media block) — update all three together. */ body.magicauth-mode-dark { --magicauth-color-page: #0e1116; - --magicauth-color-surface: #181c22; + --magicauth-color-surface: #181c22; /* SYNC with Settings::DARK_SURFACE_HEX */ --magicauth-color-code-bg: #202832; --magicauth-color-text: #eef0f3; --magicauth-color-text-muted: #9aa3ad; @@ -465,11 +471,15 @@ body.magicauth-mode-dark { } /* Dark mode — auto. Only kicks in when admin chose color_mode = auto AND - the visitor's browser reports a dark preference. */ + the visitor's browser reports a dark preference. + !!! SYNC-LOCK !!! --magicauth-color-surface (#181c22) MUST equal the PHP + constant MagicAuth\Admin\Settings::DARK_SURFACE_HEX + (includes/Admin/Settings.php:~42). See full note above the explicit-dark + block (~line 451). Three locations to update in lockstep. */ @media (prefers-color-scheme: dark) { body.magicauth-mode-auto { --magicauth-color-page: #0e1116; - --magicauth-color-surface: #181c22; + --magicauth-color-surface: #181c22; /* SYNC with Settings::DARK_SURFACE_HEX */ --magicauth-color-code-bg: #202832; --magicauth-color-text: #eef0f3; --magicauth-color-text-muted: #9aa3ad; diff --git a/assets/js/magicauth-admin.js b/assets/js/magicauth-admin.js index 67b59ce..497b670 100644 --- a/assets/js/magicauth-admin.js +++ b/assets/js/magicauth-admin.js @@ -148,6 +148,11 @@ mirrorFromSource(); } } ); + + // Initial render: if follower is blank, mirror immediately. + if ( isConnected() ) { + mirrorFromSource(); + } } ); } diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php index b33921d..b81960a 100644 --- a/includes/Admin/Settings.php +++ b/includes/Admin/Settings.php @@ -32,9 +32,13 @@ final class Settings { private const COLOR_MODE_KEYS = [ 'light', 'dark', 'auto' ]; - // Curated dark-mode surface — must match the value emitted by magicauth.css - // for body.magicauth-mode-dark / .magicauth-mode-auto. Used by the - // brand-color contrast check below. + // Curated dark-mode surface — used by the brand-color contrast check below. + // !!! SYNC-LOCK !!! This value MUST equal the --magicauth-color-surface + // declarations in assets/css/magicauth.css at: + // - line ~463 (body.magicauth-mode-dark block) + // - line ~482 (@media (prefers-color-scheme: dark) > body.magicauth-mode-auto) + // Three locations, one truth. If you change one, change all three or the + // contrast warning shown to admins will disagree with the rendered card. private const DARK_SURFACE_HEX = '#181c22'; // Keyed by extension regex (wp_check_filetype_and_ext format); membership checks hit values. @@ -53,6 +57,10 @@ final class Settings { 'webp' => 'image/webp', ]; + // Background image soft-limit. Files larger than this trigger a non-blocking + // "consider compressing" admin notice but still save successfully. + private const BACKGROUND_SIZE_SOFT_LIMIT_BYTES = 1024 * 1024; // 1 MB. + public static function setup(): void { add_action( 'admin_init', [ self::class, 'register' ] ); add_action( 'admin_menu', [ self::class, 'menu' ] ); @@ -654,14 +662,14 @@ private static function sanitize_background( int $attachment_id ): int { } $size = (int) @filesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - if ( $size > 1024 * 1024 ) { + if ( $size > self::BACKGROUND_SIZE_SOFT_LIMIT_BYTES ) { add_settings_error( self::OPTION_NAME, 'magicauth_bg_large', sprintf( /* translators: %s: file size in MB */ __( 'Background image saved. File is %s MB — consider compressing for faster page load.', 'magicauth' ), - number_format( $size / ( 1024 * 1024 ), 1 ) + number_format( $size / self::BACKGROUND_SIZE_SOFT_LIMIT_BYTES, 1 ) ), 'warning' );