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' => [[