Refactor layout components to improve responsiveness and accessibility; integrate appShell classes for consistent styling across DesktopAIShell and BottomNav. Update SelectPage to enhance option selection UI with image support and language filtering logic in SystemPage for better localization handling.

This commit is contained in:
yangy
2026-05-25 16:30:40 +08:00
parent 961c3b4de9
commit c34571392d
18 changed files with 130 additions and 55 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+5 -3
View File
@@ -22,6 +22,7 @@ import AIPage from "./pages/AIPage";
import CommunityPage from "./pages/CommunityPage"; import CommunityPage from "./pages/CommunityPage";
import { AIDrawerProvider } from "./contexts/AIDrawerContext"; import { AIDrawerProvider } from "./contexts/AIDrawerContext";
import DesktopAIShell from "./components/DesktopAIShell"; import DesktopAIShell from "./components/DesktopAIShell";
import { appShellColumnClass, appShellGutterClass } from "./lib/appShell";
function AIPageRoute() { function AIPageRoute() {
return <AIPage />; return <AIPage />;
@@ -59,9 +60,10 @@ function App() {
<Toaster /> <Toaster />
<Router hook={useHashLocation}> <Router hook={useHashLocation}>
<AIDrawerProvider> <AIDrawerProvider>
{/* Desktop: narrow shell centered in the browser with side gutters */} <div
<div className="min-h-dvh bg-[#0d1117]"> className={`min-h-dvh bg-[#0d1117] flex justify-center ${appShellGutterClass}`}
<div className="mx-auto min-h-dvh w-full max-w-full lg:max-w-[min(1180px,calc(100vw-3rem))]"> >
<div className={`min-h-dvh ${appShellColumnClass}`}>
<DesktopAIShell> <DesktopAIShell>
<AppRouter /> <AppRouter />
</DesktopAIShell> </DesktopAIShell>
+14 -9
View File
@@ -22,6 +22,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { useMemo } from "react"; import { useMemo } from "react";
import { appShellColumnClass, appShellGutterClass } from "@/lib/appShell";
type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>; type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>;
@@ -56,16 +57,19 @@ export default function BottomNav() {
return ( return (
<nav <nav
className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2" className={`fixed inset-x-0 bottom-0 z-50 flex justify-center pointer-events-none ${appShellGutterClass}`}
style={{
background: "rgba(10, 10, 12, 0.96)",
backdropFilter: "blur(40px) saturate(200%)",
WebkitBackdropFilter: "blur(40px) saturate(200%)",
borderTop: "0.5px solid rgba(255,255,255,0.1)",
boxShadow: "0 -1px 0 rgba(0,255,246,0.05)",
paddingBottom: "env(safe-area-inset-bottom, 8px)",
}}
> >
<div
className={`pointer-events-auto ${appShellColumnClass}`}
style={{
background: "rgba(10, 10, 12, 0.96)",
backdropFilter: "blur(40px) saturate(200%)",
WebkitBackdropFilter: "blur(40px) saturate(200%)",
borderTop: "0.5px solid rgba(255,255,255,0.1)",
boxShadow: "0 -1px 0 rgba(0,255,246,0.05)",
paddingBottom: "env(safe-area-inset-bottom, 8px)",
}}
>
<div className="flex items-stretch justify-around"> <div className="flex items-stretch justify-around">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const isActive = const isActive =
@@ -192,6 +196,7 @@ export default function BottomNav() {
); );
})} })}
</div> </div>
</div>
</nav> </nav>
); );
} }
+3 -1
View File
@@ -92,7 +92,9 @@ export default function DesktopAIShell({ children }: { children: ReactNode }) {
"min-w-0", "min-w-0",
showSplit showSplit
? "h-full min-h-0 overflow-y-auto overflow-x-hidden" ? "h-full min-h-0 overflow-y-auto overflow-x-hidden"
: "min-h-dvh max-h-dvh overflow-x-hidden overflow-y-auto overscroll-y-contain [touch-action:pan-y]", : isLgUp
? "min-h-dvh overflow-x-hidden"
: "min-h-dvh max-h-dvh overflow-x-hidden overflow-y-auto overscroll-y-contain [touch-action:pan-y]",
)} )}
style={ style={
showSplit showSplit
+13
View File
@@ -116,12 +116,25 @@
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
html,
body,
#root {
min-height: 100%;
}
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: -apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif; font-family: -apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
background-color: #000000; background-color: #000000;
} }
/* Desktop / ultrawide: page scrolls on window; shell stays centered */
@media (min-width: 1024px) {
html {
overflow-x: hidden;
}
}
button:not(:disabled), button:not(:disabled),
[role="button"]:not([aria-disabled="true"]), [role="button"]:not([aria-disabled="true"]),
[type="button"]:not(:disabled), [type="button"]:not(:disabled),
+8
View File
@@ -0,0 +1,8 @@
/** Centered phone-style shell width (desktop / ultrawide). */
export const APP_SHELL_MAX_PX = 1180;
/** Inner column inside horizontal gutters — use with a flex/grid centering parent. */
export const appShellColumnClass =
"w-full min-w-0 max-w-[min(1180px,100%)]";
export const appShellGutterClass = "px-4 sm:px-6";
+1 -13
View File
@@ -546,19 +546,7 @@
}, },
{ {
"index": 2, "index": 2,
"label": "Chinese" "label": "Simplified Chinese"
},
{
"index": 3,
"label": "German"
},
{
"index": 4,
"label": "Spanish"
},
{
"index": 5,
"label": "French"
} }
] ]
}, },
+1 -4
View File
@@ -384,10 +384,7 @@
"options": [ "options": [
{ "index": 0, "label": "英語" }, { "index": 0, "label": "英語" },
{ "index": 1, "label": "中文(繁體)" }, { "index": 1, "label": "中文(繁體)" },
{ "index": 2, "label": "中文(簡體)" }, { "index": 2, "label": "中文(簡體)" }
{ "index": 3, "label": "德語" },
{ "index": 4, "label": "西語" },
{ "index": 5, "label": "法語" }
] ]
}, },
"Automatic": { "Automatic": {
+1 -4
View File
@@ -385,10 +385,7 @@
"options": [ "options": [
{ "index": 0, "label": "英语" }, { "index": 0, "label": "英语" },
{ "index": 1, "label": "中文(繁体)" }, { "index": 1, "label": "中文(繁体)" },
{ "index": 2, "label": "中文(简体)" }, { "index": 2, "label": "中文(简体)" }
{ "index": 3, "label": "德语" },
{ "index": 4, "label": "西班牙语" },
{ "index": 5, "label": "法语" }
] ]
}, },
"Automatic": { "Automatic": {
+65 -19
View File
@@ -13,8 +13,12 @@
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { ChevronLeft, Check } from "lucide-react"; import { ChevronLeft, Check } from "lucide-react";
import { useDevice } from "@/contexts/DeviceContext"; import { useDevice } from "@/contexts/DeviceContext";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const iisModeImageUrl = (index: number) =>
`${import.meta.env.BASE_URL}IIS/iis_app${index + 1}.png`;
/* ── Key → API field mapping ── */ /* ── Key → API field mapping ── */
const KEY_TO_SETTING: Record<string, string> = { const KEY_TO_SETTING: Record<string, string> = {
effect_value: "effect_value", effect_value: "effect_value",
@@ -118,26 +122,68 @@ export default function SelectPage() {
<div className="w-8" /> <div className="w-8" />
</div> </div>
{/* ── Options list ── */}
<div className="px-4 pt-4 pb-24"> <div className="px-4 pt-4 pb-24">
<div className="ios-list-group"> {key === "IISMode" ? (
{options.map((opt, idx) => { <div className="grid w-full grid-cols-2 gap-3">
const isSelected = idx === selectedIdx; {options.map((opt, idx) => {
return ( const isSelected = idx === selectedIdx;
<button return (
key={idx} <button
className="ios-list-row w-full text-left active:bg-white/5 transition-colors" key={idx}
onClick={() => void handleSelect(idx)}> type="button"
<span className={`flex-1 text-[16px] ${isSelected ? "text-white font-medium" : "text-white/75"}`}> disabled={saving}
{opt} className={cn(
</span> "flex flex-col items-center gap-2 rounded-xl border-2 p-1.5 transition-all active:scale-[0.98] disabled:opacity-60",
{isSelected && ( isSelected
<Check size={18} style={{ color: "#00FFF6" }} /> ? "border-[#00FFF6] shadow-[0_0_14px_rgba(0,255,246,0.35)]"
)} : "border-white/10",
</button> )}
); onClick={() => void handleSelect(idx)}
})} aria-pressed={isSelected}
</div> aria-label={opt}
>
<img
src={iisModeImageUrl(idx)}
alt={opt}
className="block w-full h-auto rounded-lg bg-black/40"
loading="lazy"
/>
<span
className={cn(
"w-6 h-6 rounded-full flex items-center justify-center shrink-0",
isSelected
? "bg-[#00FFF6] text-black shadow-[0_0_10px_rgba(0,255,246,0.45)]"
: "border-2 border-white/20",
)}
>
{isSelected && <Check size={12} strokeWidth={3} />}
</span>
</button>
);
})}
</div>
) : (
<div className="ios-list-group">
{options.map((opt, idx) => {
const isSelected = idx === selectedIdx;
return (
<button
key={idx}
className="ios-list-row w-full text-left active:bg-white/5 transition-colors"
onClick={() => void handleSelect(idx)}
disabled={saving}
>
<span className={`flex-1 text-[16px] ${isSelected ? "text-white font-medium" : "text-white/75"}`}>
{opt}
</span>
{isSelected && (
<Check size={18} style={{ color: "#00FFF6" }} />
)}
</button>
);
})}
</div>
)}
</div> </div>
</div> </div>
); );
+19 -2
View File
@@ -107,6 +107,18 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined
return sorted.map((item) => item.label); return sorted.map((item) => item.label);
} }
/** Device language index: 0=en, 1=zh-HK, 2=zh-CN */
const SUPPORTED_LANGUAGE_INDICES = new Set([0, 1, 2]);
function languageLabels(
items: Array<{ index: number; label: string }> | undefined,
fallback: string[],
) {
const filtered = (items ?? []).filter((o) => SUPPORTED_LANGUAGE_INDICES.has(o.index));
if (filtered.length === 0) return fallback;
return [...filtered].sort((a, b) => a.index - b.index).map((o) => o.label);
}
export default function SystemPage() { export default function SystemPage() {
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();
const { deviceState, updateSetting, refresh, isConnected, api, ip } = useDevice(); const { deviceState, updateSetting, refresh, isConnected, api, ip } = useDevice();
@@ -124,7 +136,8 @@ export default function SystemPage() {
const screenOffIdx = ds?.screenOff ?? 2; const screenOffIdx = ds?.screenOff ?? 2;
const sleepIdx = ds?.sleep ?? 0; const sleepIdx = ds?.sleep ?? 0;
const autoHomeIdx = ds?.autoHome ?? 1; const autoHomeIdx = ds?.autoHome ?? 1;
const langIdx = ds?.language ?? 2; const rawLangIdx = ds?.language ?? 2;
const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : 2;
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0; const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh; const localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
const settingsText = localeData.settings; const settingsText = localeData.settings;
@@ -134,7 +147,11 @@ export default function SystemPage() {
const SCREEN_OFF_OPTIONS = optionLabels(settingsText.turnOffScreen?.options, ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]); const SCREEN_OFF_OPTIONS = optionLabels(settingsText.turnOffScreen?.options, ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]);
const SLEEP_OPTIONS = optionLabels(settingsText.sleepTime?.options ?? settingsText.sleep?.options, ["关闭", "1分钟", "5分钟", "10分钟"]); const SLEEP_OPTIONS = optionLabels(settingsText.sleepTime?.options ?? settingsText.sleep?.options, ["关闭", "1分钟", "5分钟", "10分钟"]);
const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]); const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]);
const LANG_OPTIONS = optionLabels(settingsText.language?.options, ["English", "繁體中文", "简体中文"]); const LANG_OPTIONS = languageLabels(settingsText.language?.options, [
"English",
"繁體中文",
"简体中文",
]);
const sleepRowLabel = const sleepRowLabel =
settingsText.sleepTime?.label ?? settingsText.sleepTime?.label ??