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.
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -22,6 +22,7 @@ import AIPage from "./pages/AIPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
||||
import DesktopAIShell from "./components/DesktopAIShell";
|
||||
import { appShellColumnClass, appShellGutterClass } from "./lib/appShell";
|
||||
|
||||
function AIPageRoute() {
|
||||
return <AIPage />;
|
||||
@@ -59,9 +60,10 @@ function App() {
|
||||
<Toaster />
|
||||
<Router hook={useHashLocation}>
|
||||
<AIDrawerProvider>
|
||||
{/* Desktop: narrow shell centered in the browser with side gutters */}
|
||||
<div className="min-h-dvh bg-[#0d1117]">
|
||||
<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 bg-[#0d1117] flex justify-center ${appShellGutterClass}`}
|
||||
>
|
||||
<div className={`min-h-dvh ${appShellColumnClass}`}>
|
||||
<DesktopAIShell>
|
||||
<AppRouter />
|
||||
</DesktopAIShell>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useMemo } from "react";
|
||||
import { appShellColumnClass, appShellGutterClass } from "@/lib/appShell";
|
||||
|
||||
type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>;
|
||||
|
||||
@@ -56,16 +57,19 @@ export default function BottomNav() {
|
||||
|
||||
return (
|
||||
<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"
|
||||
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)",
|
||||
}}
|
||||
className={`fixed inset-x-0 bottom-0 z-50 flex justify-center pointer-events-none ${appShellGutterClass}`}
|
||||
>
|
||||
<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">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive =
|
||||
@@ -192,6 +196,7 @@ export default function BottomNav() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ export default function DesktopAIShell({ children }: { children: ReactNode }) {
|
||||
"min-w-0",
|
||||
showSplit
|
||||
? "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={
|
||||
showSplit
|
||||
|
||||
@@ -116,12 +116,25 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: -apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
/* Desktop / ultrawide: page scrolls on window; shell stays centered */
|
||||
@media (min-width: 1024px) {
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
button:not(:disabled),
|
||||
[role="button"]:not([aria-disabled="true"]),
|
||||
[type="button"]:not(:disabled),
|
||||
|
||||
@@ -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";
|
||||
@@ -546,19 +546,7 @@
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"label": "Chinese"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"label": "German"
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"label": "Spanish"
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"label": "French"
|
||||
"label": "Simplified Chinese"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -384,10 +384,7 @@
|
||||
"options": [
|
||||
{ "index": 0, "label": "英語" },
|
||||
{ "index": 1, "label": "中文(繁體)" },
|
||||
{ "index": 2, "label": "中文(簡體)" },
|
||||
{ "index": 3, "label": "德語" },
|
||||
{ "index": 4, "label": "西語" },
|
||||
{ "index": 5, "label": "法語" }
|
||||
{ "index": 2, "label": "中文(簡體)" }
|
||||
]
|
||||
},
|
||||
"Automatic": {
|
||||
|
||||
@@ -385,10 +385,7 @@
|
||||
"options": [
|
||||
{ "index": 0, "label": "英语" },
|
||||
{ "index": 1, "label": "中文(繁体)" },
|
||||
{ "index": 2, "label": "中文(简体)" },
|
||||
{ "index": 3, "label": "德语" },
|
||||
{ "index": 4, "label": "西班牙语" },
|
||||
{ "index": 5, "label": "法语" }
|
||||
{ "index": 2, "label": "中文(简体)" }
|
||||
]
|
||||
},
|
||||
"Automatic": {
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
import { useLocation } from "wouter";
|
||||
import { ChevronLeft, Check } from "lucide-react";
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const iisModeImageUrl = (index: number) =>
|
||||
`${import.meta.env.BASE_URL}IIS/iis_app${index + 1}.png`;
|
||||
|
||||
/* ── Key → API field mapping ── */
|
||||
const KEY_TO_SETTING: Record<string, string> = {
|
||||
effect_value: "effect_value",
|
||||
@@ -118,26 +122,68 @@ export default function SelectPage() {
|
||||
<div className="w-8" />
|
||||
</div>
|
||||
|
||||
{/* ── Options list ── */}
|
||||
<div className="px-4 pt-4 pb-24">
|
||||
<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)}>
|
||||
<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>
|
||||
{key === "IISMode" ? (
|
||||
<div className="grid w-full grid-cols-2 gap-3">
|
||||
{options.map((opt, idx) => {
|
||||
const isSelected = idx === selectedIdx;
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-2 rounded-xl border-2 p-1.5 transition-all active:scale-[0.98] disabled:opacity-60",
|
||||
isSelected
|
||||
? "border-[#00FFF6] shadow-[0_0_14px_rgba(0,255,246,0.35)]"
|
||||
: "border-white/10",
|
||||
)}
|
||||
onClick={() => void handleSelect(idx)}
|
||||
aria-pressed={isSelected}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -107,6 +107,18 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined
|
||||
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() {
|
||||
const [, setLocation] = useLocation();
|
||||
const { deviceState, updateSetting, refresh, isConnected, api, ip } = useDevice();
|
||||
@@ -124,7 +136,8 @@ export default function SystemPage() {
|
||||
const screenOffIdx = ds?.screenOff ?? 2;
|
||||
const sleepIdx = ds?.sleep ?? 0;
|
||||
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 localeData = LOCALE_BY_LANG[langIdx] ?? localeZh;
|
||||
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 SLEEP_OPTIONS = optionLabels(settingsText.sleepTime?.options ?? settingsText.sleep?.options, ["关闭", "1分钟", "5分钟", "10分钟"]);
|
||||
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 =
|
||||
settingsText.sleepTime?.label ??
|
||||
|
||||