diff --git a/assets/css/frontend/base.css b/assets/css/frontend/base.css index 27281377..6d6ef2a1 100644 --- a/assets/css/frontend/base.css +++ b/assets/css/frontend/base.css @@ -42,6 +42,13 @@ a[href] { transition: color 0.2s, background 0.3s, opacity 0.3s, box-shadow 0.3s, transform 0.3s; } +/* Font Awesome 5: when legacy markup includes both `fa` and `fab`, the + * generic Solid rule must not override the Brands font face. */ +.fa.fab { + font-family: "Font Awesome 5 Brands"; + font-weight: 400; +} + .skip-link { position: absolute; left: 16px; diff --git a/src/Admin/SchemaProvider.php b/src/Admin/SchemaProvider.php index 10bd4351..66b4ae93 100644 --- a/src/Admin/SchemaProvider.php +++ b/src/Admin/SchemaProvider.php @@ -215,6 +215,13 @@ class SchemaProvider { } if (in_array($type, ['select', 'radio', 'palette', 'image_select'], true) && !empty($field['options'])) { + // Optional enum fields use an empty string to mean "unset". + // This is required for clearable palettes such as the footer + // background preset, whose default is intentionally empty. + if ($value === '' && empty($field['required'])) { + continue; + } + $valid_values = array_column($field['options'], 'value'); // Multi-select: accept array values when schema.multiple is set // (legacy / SPA may still use type=select multiple). diff --git a/tests/widget-system/page-display-contract.test.cjs b/tests/widget-system/page-display-contract.test.cjs index a6c8ddc8..63ff512b 100644 --- a/tests/widget-system/page-display-contract.test.cjs +++ b/tests/widget-system/page-display-contract.test.cjs @@ -14,6 +14,7 @@ const provider = read('src/Admin/SchemaProvider.php') const settingsFacade = read('src/Settings/Settings.php') const scriptsPart1c = read('src/Core/ScriptsPart1c.php') const footerCss = read('assets/css/frontend/footer/zibll-shell.css') +const frontendBaseCss = read('assets/css/frontend/base.css') const headerMenuButtons = read('inc/helpers/aksrt-header-menu-buttons.php') function activeRepositorySource() { @@ -178,6 +179,17 @@ test('front-end page-display consumers read only the canonical section contracts assert.doesNotMatch(scriptsPart1c, /\$runtime_routes\[['"]cart['"]\]/) }) +test('footer brand icons keep the Brands font when legacy fa prefix is present', () => { + const footer = read('footer.php') + const icons = read('inc/helpers/aksrt-icons.php') + + assert.match(footer, /class="fa fab fa-weixin"/) + assert.match(footer, /class="fa fab fa-qq"/) + assert.match(footer, /class="fa fab fa-weibo"/) + assert.match(frontendBaseCss, /\.fa\.fab\s*\{[^}]*font-family:\s*"Font Awesome 5 Brands"[^}]*font-weight:\s*400/s) + assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/) +}) + test('save contract rejects invalid input and round-trips canonical nested values', () => { const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'page-display-save-harness.php')], { cwd: root, diff --git a/tests/widget-system/page-display-save-harness.php b/tests/widget-system/page-display-save-harness.php index ce083783..84cdf6f4 100644 --- a/tests/widget-system/page-display-save-harness.php +++ b/tests/widget-system/page-display-save-harness.php @@ -167,6 +167,8 @@ namespace { $header_clean = $provider->sanitize_payload('page_display', $header_payload, 'header-components'); $footer_payload = [ + // Empty means no preset background; it must remain a valid optional palette value. + 'footer_theme_bg' => '', 'fcode_t1_code' => 'Allowed', 'footer_contact_weibo' => 'javascript:alert(1)', 'footer_mini_img' => [[