feat: refine responsive hero and preference dropdowns

This commit is contained in:
2026-08-15 18:53:54 +08:00
parent af296d674f
commit 073a739f10
3 changed files with 156 additions and 66 deletions
+1 -1
View File
@@ -4,4 +4,4 @@ services:
container_name: roncarve-web container_name: roncarve-web
restart: unless-stopped restart: unless-stopped
ports: ports:
- "127.0.0.1:8080:80" - "127.0.0.1:8081:80"
+149 -64
View File
@@ -207,57 +207,81 @@
.nav-link.active::after { opacity: 1; transform: translateX(-50%) scale(1); } .nav-link.active::after { opacity: 1; transform: translateX(-50%) scale(1); }
.nav-actions { display: flex; align-items: center; gap: 10px; } .nav-actions { display: flex; align-items: center; gap: 10px; }
.theme-switch { .preference-menu { position: relative; }
.preference-trigger {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 3px; gap: 8px;
min-height: 38px; min-height: 38px;
padding: 3px; padding: 0 11px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 9px; border-radius: 9px;
background: rgba(255, 255, 255, .025); background: rgba(255, 255, 255, .025);
transition: border-color .2s ease, background .2s ease; color: var(--text-soft);
font: 600 12px/1 var(--mono);
cursor: pointer;
transition: border-color .2s ease, background .2s ease, color .2s ease;
} }
.theme-switch:hover { border-color: var(--line-strong); background: rgba(54, 228, 218, .04); } .preference-trigger:hover,
.theme-option { .preference-menu.open .preference-trigger {
position: relative; border-color: var(--line-strong);
background: rgba(54, 228, 218, .06);
color: var(--text);
}
.preference-icon {
display: inline-grid; display: inline-grid;
place-items: center; place-items: center;
width: 16px;
height: 16px;
color: var(--text);
font: 500 17px/1 "Segoe UI Symbol", "Noto Sans Symbols", sans-serif;
}
.preference-icon--globe { font-size: 16px; }
.preference-chevron { color: var(--text-faint); font: 700 13px/1 var(--mono); transition: transform .2s ease; }
.preference-menu.open .preference-chevron { transform: rotate(180deg); }
.preference-popover {
position: absolute;
top: calc(100% + 10px);
right: 0;
z-index: 1100;
display: grid;
min-width: 174px;
padding: 6px;
border: 1px solid var(--line-strong);
border-radius: 10px;
background: color-mix(in srgb, var(--ink-900) 94%, transparent);
box-shadow: 0 18px 44px rgba(0, 0, 0, .2), 0 0 0 1px rgba(255, 255, 255, .035);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
opacity: 0;
pointer-events: none;
transform: translateY(-6px) scale(.98);
transform-origin: top right;
transition: opacity .18s ease, transform .18s var(--ease);
}
.preference-menu.open .preference-popover { opacity: 1; pointer-events: auto; transform: translateY(0) scale(1); }
.preference-item {
display: flex;
align-items: center; align-items: center;
width: 30px; gap: 10px;
height: 30px; width: 100%;
padding: 0; min-height: 38px;
padding: 0 10px;
border: 0; border: 0;
border-radius: 6px; border-radius: 7px;
background: transparent; background: transparent;
color: var(--text-soft); color: var(--text-soft);
font: 600 12px/1.4 var(--mono);
text-align: left;
cursor: pointer; cursor: pointer;
transition: background .2s ease, box-shadow .2s ease, transform .2s ease; transition: color .18s ease, background .18s ease;
} }
.theme-option:hover { background: color-mix(in srgb, var(--cyan) 8%, transparent); transform: translateY(-1px); } .preference-item:hover,
.theme-option[aria-checked="true"] { background: color-mix(in srgb, var(--cyan) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cyan) 28%, transparent); } .preference-item[aria-checked="true"] { color: var(--text); background: color-mix(in srgb, var(--cyan) 12%, transparent); }
.theme-option-icon { width: 13px; height: 13px; border: 1px solid var(--line-strong); border-radius: 50%; box-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 16%, transparent); } .preference-item[aria-checked="true"] { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cyan) 25%, transparent); }
.theme-option-icon.system { background: linear-gradient(90deg, #f7fbff 0 49%, #0a1721 51% 100%); } .preference-item-label { flex: 1; }
.theme-option-icon.light { background: #eef7ff; box-shadow: 0 0 0 2px rgba(51, 92, 255, .11), 0 0 9px rgba(74, 141, 255, .22); } .preference-item-check { color: var(--cyan); opacity: 0; font-size: 13px; }
.theme-option-icon.dark { background: #07131d; box-shadow: inset -3px 0 0 #54ddea, 0 0 9px rgba(54, 228, 218, .18); } .preference-item[aria-checked="true"] .preference-item-check { opacity: 1; }
.lang-switch {
display: inline-flex;
padding: 3px;
border: 1px solid var(--line);
border-radius: 9px;
background: rgba(255, 255, 255, .025);
}
.lang-btn {
min-width: 34px;
padding: 5px 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--text-faint);
font: 650 12px/1.4 var(--mono);
cursor: pointer;
}
.lang-btn.active { color: var(--ink-1000); background: var(--cyan); }
.nav-cta, .button { .nav-cta, .button {
display: inline-flex; display: inline-flex;
@@ -694,12 +718,10 @@
:root[data-theme="light"] .navbar { background: rgba(250, 252, 255, .72); } :root[data-theme="light"] .navbar { background: rgba(250, 252, 255, .72); }
:root[data-theme="light"] .navbar.scrolled { background: rgba(250, 252, 255, .93); box-shadow: 0 12px 40px rgba(35, 53, 87, .08); } :root[data-theme="light"] .navbar.scrolled { background: rgba(250, 252, 255, .93); box-shadow: 0 12px 40px rgba(35, 53, 87, .08); }
:root[data-theme="light"] .lang-switch, :root[data-theme="light"] .preference-trigger,
:root[data-theme="light"] .theme-switch,
:root[data-theme="light"] .menu-btn { background: rgba(255, 255, 255, .58); } :root[data-theme="light"] .menu-btn { background: rgba(255, 255, 255, .58); }
:root[data-theme="light"] .nav-cta, :root[data-theme="light"] .nav-cta,
:root[data-theme="light"] .button-primary, :root[data-theme="light"] .button-primary { color: #fff; }
:root[data-theme="light"] .lang-btn.active { color: #fff; }
:root[data-theme="light"] .button-secondary { background: rgba(255, 255, 255, .58); } :root[data-theme="light"] .button-secondary { background: rgba(255, 255, 255, .58); }
:root[data-theme="light"] .hero { :root[data-theme="light"] .hero {
@@ -733,6 +755,13 @@
.hero-spatial { width: min(100%, 760px); min-height: 420px; margin-left: auto; } .hero-spatial { width: min(100%, 760px); min-height: 420px; margin-left: auto; }
} }
/* Let desktop layouts use the available canvas on ultrawide screens. */
@media (min-width: 1440px) {
:root { --max-width: 88vw; }
.hero-layout { gap: clamp(64px, 6vw, 132px); }
.section-header { gap: clamp(48px, 6vw, 120px); }
}
@media (max-width: 860px) { @media (max-width: 860px) {
:root[data-theme="light"] .nav-links { background: rgba(250, 252, 255, .98); } :root[data-theme="light"] .nav-links { background: rgba(250, 252, 255, .98); }
} }
@@ -901,20 +930,38 @@
<a class="nav-link" href="#advantages" data-i18n="nav_advantages">核心优势</a> <a class="nav-link" href="#advantages" data-i18n="nav_advantages">核心优势</a>
<a class="nav-link" href="#offices" data-i18n="nav_offices">交付网络</a> <a class="nav-link" href="#offices" data-i18n="nav_offices">交付网络</a>
<div class="nav-actions"> <div class="nav-actions">
<div class="theme-switch" id="themeSwitch" role="radiogroup" aria-label="外观模式" data-i18n-aria="theme_label"> <div class="preference-menu" id="themeSwitch">
<button class="theme-option" type="button" role="radio" aria-checked="true" data-theme-option="system" aria-label="跟随系统" data-i18n-aria="theme_system"> <button class="preference-trigger" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="外观模式" data-i18n-aria="theme_label">
<span class="theme-option-icon system" aria-hidden="true"></span> <span class="preference-icon" data-theme-current-icon aria-hidden="true"></span>
</button> <span class="preference-trigger-label" data-theme-current>Auto</span>
<button class="theme-option" type="button" role="radio" aria-checked="false" data-theme-option="light" aria-label="浅色外观" data-i18n-aria="theme_light"> <span class="preference-chevron" aria-hidden="true"></span>
<span class="theme-option-icon light" aria-hidden="true"></span>
</button>
<button class="theme-option" type="button" role="radio" aria-checked="false" data-theme-option="dark" aria-label="深色外观" data-i18n-aria="theme_dark">
<span class="theme-option-icon dark" aria-hidden="true"></span>
</button> </button>
<div class="preference-popover" role="menu">
<button class="preference-item" type="button" role="menuitemradio" aria-checked="true" data-theme-option="system" aria-label="跟随系统" data-i18n-aria="theme_system">
<span class="preference-icon" aria-hidden="true"></span><span class="preference-item-label" data-i18n="theme_system_short">Auto</span><span class="preference-item-check" aria-hidden="true"></span>
</button>
<button class="preference-item" type="button" role="menuitemradio" aria-checked="false" data-theme-option="dark" aria-label="深色外观" data-i18n-aria="theme_dark">
<span class="preference-icon" aria-hidden="true"></span><span class="preference-item-label" data-i18n="theme_dark_short">Dark</span><span class="preference-item-check" aria-hidden="true"></span>
</button>
<button class="preference-item" type="button" role="menuitemradio" aria-checked="false" data-theme-option="light" aria-label="浅色外观" data-i18n-aria="theme_light">
<span class="preference-icon" aria-hidden="true"></span><span class="preference-item-label" data-i18n="theme_light_short">Light</span><span class="preference-item-check" aria-hidden="true"></span>
</button>
</div>
</div> </div>
<div class="lang-switch" id="langSwitch" aria-label="语言切换" data-i18n-aria="lang_label"> <div class="preference-menu" id="langSwitch">
<button class="lang-btn active" type="button" data-lang="zh" aria-pressed="true"></button> <button class="preference-trigger" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="语言切换" data-i18n-aria="lang_label">
<button class="lang-btn" type="button" data-lang="en" aria-pressed="false">EN</button> <span class="preference-icon preference-icon--globe" aria-hidden="true"></span>
<span class="preference-trigger-label" data-language-current>简体中文</span>
<span class="preference-chevron" aria-hidden="true"></span>
</button>
<div class="preference-popover" role="menu">
<button class="preference-item" type="button" role="menuitemradio" aria-checked="true" data-lang="zh">
<span class="preference-icon preference-icon--globe" aria-hidden="true"></span><span class="preference-item-label" data-i18n="language_zh">简体中文</span><span class="preference-item-check" aria-hidden="true"></span>
</button>
<button class="preference-item" type="button" role="menuitemradio" aria-checked="false" data-lang="en">
<span class="preference-icon preference-icon--globe" aria-hidden="true"></span><span class="preference-item-label" data-i18n="language_en">English</span><span class="preference-item-check" aria-hidden="true"></span>
</button>
</div>
</div> </div>
<a class="nav-cta" href="#contact" data-i18n="nav_contact">预约咨询</a> <a class="nav-cta" href="#contact" data-i18n="nav_contact">预约咨询</a>
</div> </div>
@@ -1109,7 +1156,7 @@
const i18n = { const i18n = {
zh: { zh: {
title: '海南荣刻科技有限公司 - 数据筑基 · AI 落地', skip: '跳到主要内容', brand: '荣刻科技', nav_label: '主导航', home_label: '荣刻科技首页', lang_label: '语言切换', menu_open: '打开导航菜单', menu_close: '关闭导航菜单', title: '海南荣刻科技有限公司 - 数据筑基 · AI 落地', skip: '跳到主要内容', brand: '荣刻科技', nav_label: '主导航', home_label: '荣刻科技首页', lang_label: '语言切换', menu_open: '打开导航菜单', menu_close: '关闭导航菜单',
theme_label: '外观模式选择', theme_system: '跟随系统', theme_light: '浅色外观', theme_dark: '深色外观', theme_label: '外观模式选择', theme_system: '跟随系统', theme_light: '浅色外观', theme_dark: '深色外观', theme_system_short: 'Auto', theme_light_short: 'Light', theme_dark_short: 'Dark', language_zh: '简体中文', language_en: 'English',
nav_about: '能力链路', nav_services: '解决方案', nav_advantages: '核心优势', nav_offices: '交付网络', nav_contact: '预约咨询', nav_about: '能力链路', nav_services: '解决方案', nav_advantages: '核心优势', nav_offices: '交付网络', nav_contact: '预约咨询',
hero_eyebrow: 'AI 与数据基础设施', hero_line1: '让企业数据', hero_line2: '成为 AI 生产力', hero_eyebrow: 'AI 与数据基础设施', hero_line1: '让企业数据', hero_line2: '成为 AI 生产力',
hero_desc: '从数据治理、模型优化到私有化部署,荣刻科技为企业构建可落地、可持续、可掌控的 AI 能力体系。', hero_desc: '从数据治理、模型优化到私有化部署,荣刻科技为企业构建可落地、可持续、可掌控的 AI 能力体系。',
@@ -1143,7 +1190,7 @@
}, },
en: { en: {
title: 'Hainan Roncarve Technology - Data Foundation · AI Delivery', skip: 'Skip to main content', brand: 'Roncarve Tech', nav_label: 'Main navigation', home_label: 'Roncarve Tech home', lang_label: 'Language selector', menu_open: 'Open navigation menu', menu_close: 'Close navigation menu', title: 'Hainan Roncarve Technology - Data Foundation · AI Delivery', skip: 'Skip to main content', brand: 'Roncarve Tech', nav_label: 'Main navigation', home_label: 'Roncarve Tech home', lang_label: 'Language selector', menu_open: 'Open navigation menu', menu_close: 'Close navigation menu',
theme_label: 'Appearance selector', theme_system: 'Follow System', theme_light: 'Light appearance', theme_dark: 'Dark appearance', theme_label: 'Appearance selector', theme_system: 'Follow System', theme_light: 'Light appearance', theme_dark: 'Dark appearance', theme_system_short: 'Auto', theme_light_short: 'Light', theme_dark_short: 'Dark', language_zh: '简体中文', language_en: 'English',
nav_about: 'Capabilities', nav_services: 'Solutions', nav_advantages: 'Advantages', nav_offices: 'Delivery', nav_contact: 'Book a Call', nav_about: 'Capabilities', nav_services: 'Solutions', nav_advantages: 'Advantages', nav_offices: 'Delivery', nav_contact: 'Book a Call',
hero_eyebrow: 'AI + DATA INFRASTRUCTURE', hero_line1: 'Turn enterprise data', hero_line2: 'into AI productivity', hero_eyebrow: 'AI + DATA INFRASTRUCTURE', hero_line1: 'Turn enterprise data', hero_line2: 'into AI productivity',
hero_desc: 'From data governance and model optimization to private deployment, Roncarve builds practical, sustainable and controllable enterprise AI systems.', hero_desc: 'From data governance and model optimization to private deployment, Roncarve builds practical, sustainable and controllable enterprise AI systems.',
@@ -1187,6 +1234,8 @@
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let currentLanguage = 'zh'; let currentLanguage = 'zh';
let themePreference = document.documentElement.dataset.themePreference || 'system'; let themePreference = document.documentElement.dataset.themePreference || 'system';
const themeLabels = { system: 'theme_system_short', light: 'theme_light_short', dark: 'theme_dark_short' };
const themeIcons = { system: '◐', light: '☼', dark: '☾' };
function resolvedTheme(preference) { function resolvedTheme(preference) {
return preference === 'system' ? (themeQuery.matches ? 'dark' : 'light') : preference; return preference === 'system' ? (themeQuery.matches ? 'dark' : 'light') : preference;
@@ -1201,6 +1250,10 @@
document.querySelectorAll('[data-theme-option]').forEach(function (button) { document.querySelectorAll('[data-theme-option]').forEach(function (button) {
button.setAttribute('aria-checked', String(button.dataset.themeOption === selected)); button.setAttribute('aria-checked', String(button.dataset.themeOption === selected));
}); });
const themeCurrent = document.querySelector('[data-theme-current]');
const themeCurrentIcon = document.querySelector('[data-theme-current-icon]');
if (themeCurrent) themeCurrent.textContent = i18n[currentLanguage][themeLabels[selected]];
if (themeCurrentIcon) themeCurrentIcon.textContent = themeIcons[selected];
const themeColors = { light: '#f7faff', dark: '#05070a' }; const themeColors = { light: '#f7faff', dark: '#05070a' };
const themeMeta = document.querySelector('meta[name="theme-color"]'); const themeMeta = document.querySelector('meta[name="theme-color"]');
if (themeMeta) themeMeta.setAttribute('content', themeColors[resolved]); if (themeMeta) themeMeta.setAttribute('content', themeColors[resolved]);
@@ -1224,11 +1277,17 @@
if (i18n[selected][key]) element.setAttribute('aria-label', i18n[selected][key]); if (i18n[selected][key]) element.setAttribute('aria-label', i18n[selected][key]);
}); });
menuBtn.setAttribute('aria-label', i18n[selected][navLinks.classList.contains('open') ? 'menu_close' : 'menu_open']); menuBtn.setAttribute('aria-label', i18n[selected][navLinks.classList.contains('open') ? 'menu_close' : 'menu_open']);
document.querySelectorAll('.lang-btn').forEach(function (button) { document.querySelectorAll('[data-lang]').forEach(function (button) {
const active = button.dataset.lang === selected; const active = button.dataset.lang === selected;
button.classList.toggle('active', active); button.setAttribute('aria-checked', String(active));
button.setAttribute('aria-pressed', String(active));
}); });
const languageCurrent = document.querySelector('[data-language-current]');
if (languageCurrent) languageCurrent.textContent = i18n[selected].language_zh;
if (languageCurrent && selected === 'en') languageCurrent.textContent = i18n[selected].language_en;
if (themePreference) {
const themeCurrent = document.querySelector('[data-theme-current]');
if (themeCurrent) themeCurrent.textContent = i18n[selected][themeLabels[themePreference]];
}
try { localStorage.setItem('site_lang', selected); } catch (error) { /* Storage may be disabled. */ } try { localStorage.setItem('site_lang', selected); } catch (error) { /* Storage may be disabled. */ }
} }
@@ -1240,19 +1299,39 @@
document.body.classList.remove('menu-open'); document.body.classList.remove('menu-open');
} }
function closePreferenceMenus(except) {
document.querySelectorAll('.preference-menu.open').forEach(function (menu) {
if (menu !== except) {
menu.classList.remove('open');
menu.querySelector('.preference-trigger').setAttribute('aria-expanded', 'false');
}
});
}
[themeSwitch, langSwitch].forEach(function (menu) {
const trigger = menu.querySelector('.preference-trigger');
trigger.addEventListener('click', function () {
const open = !menu.classList.contains('open');
closePreferenceMenus(menu);
menu.classList.toggle('open', open);
trigger.setAttribute('aria-expanded', String(open));
});
});
langSwitch.addEventListener('click', function (event) { langSwitch.addEventListener('click', function (event) {
const button = event.target.closest('.lang-btn'); const button = event.target.closest('[data-lang]');
if (button) applyLanguage(button.dataset.lang); if (!button) return;
applyLanguage(button.dataset.lang);
langSwitch.classList.remove('open');
langSwitch.querySelector('.preference-trigger').setAttribute('aria-expanded', 'false');
}); });
themeSwitch.addEventListener('click', function (event) { themeSwitch.addEventListener('click', function (event) {
const button = event.target.closest('[data-theme-option]'); const button = event.target.closest('[data-theme-option]');
if (!button) return; if (!button) return;
applyTheme(button.dataset.themeOption, true); applyTheme(button.dataset.themeOption, true);
if (window.matchMedia('(max-width: 860px)').matches) { themeSwitch.classList.remove('open');
closeMenu(); themeSwitch.querySelector('.preference-trigger').setAttribute('aria-expanded', 'false');
menuBtn.focus();
}
}); });
const handleSystemTheme = function () { const handleSystemTheme = function () {
@@ -1272,7 +1351,13 @@
navLinks.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', closeMenu); }); navLinks.querySelectorAll('a').forEach(function (link) { link.addEventListener('click', closeMenu); });
document.addEventListener('keydown', function (event) { document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closeMenu(); if (event.key === 'Escape') {
closeMenu();
closePreferenceMenus();
}
});
document.addEventListener('click', function (event) {
if (!event.target.closest('.preference-menu')) closePreferenceMenus();
}); });
window.addEventListener('scroll', function () { navbar.classList.toggle('scrolled', window.scrollY > 24); }, { passive: true }); window.addEventListener('scroll', function () { navbar.classList.toggle('scrolled', window.scrollY > 24); }, { passive: true });
+6 -1
View File
@@ -783,7 +783,12 @@ if (root && canvas) {
const aspect = width / height; const aspect = width / height;
worldCamera.aspect = aspect; worldCamera.aspect = aspect;
worldCamera.fov = aspect < 0.75 ? 38 : 33; // Keep the prism's authored scale on ultrawide displays. The
// background can fill the full canvas, but the 3D composition
// should not become disproportionately small as the viewport
// gets wider than the original desktop frame.
const compositionAspect = Math.min(aspect, 2.25);
worldCamera.fov = aspect < 0.75 ? 38 : (aspect > 1.8 ? 33 * 1.8 / compositionAspect : 33);
worldCamera.updateProjectionMatrix(); worldCamera.updateProjectionMatrix();
if (aspect > 1.1) { if (aspect > 1.1) {
worldRoot.position.set(Math.min(1.92, aspect * 1.15), -0.02, 0); worldRoot.position.set(Math.min(1.92, aspect * 1.15), -0.02, 0);