fix: can't scroll misc tab on mobile

This commit is contained in:
trafficlunar 2026-04-24 18:30:35 +01:00
parent 51d46fc9ce
commit af7f1380bc
3 changed files with 294 additions and 294 deletions

View file

@ -106,7 +106,7 @@ It's a good idea to build the project locally before submitting a pull request.
$ pnpm --filter backend build $ pnpm --filter backend build
$ pnpm --filter frontend build $ pnpm --filter frontend build
# Run the built version (Vite likes to change the port when this happens, so you probably need to change both .env files) # Run the built version (Note: Vite likes to change the port when this happens, so you probably need to change both .env files)
$ pnpm --filter backend start $ pnpm --filter backend start
$ pnpm --filter frontend build $ pnpm --filter frontend build
``` ```

View file

@ -1,80 +1,80 @@
import { type SwitchMiiInstructions } from "@tomodachi-share/shared"; import { type SwitchMiiInstructions } from "@tomodachi-share/shared";
import React, { useState } from "react"; import React, { useState } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import HeadTab from "./tabs/head"; import HeadTab from "./tabs/head";
import HairTab from "./tabs/hair"; import HairTab from "./tabs/hair";
import EyebrowsTab from "./tabs/eyebrows"; import EyebrowsTab from "./tabs/eyebrows";
import EyesTab from "./tabs/eyes"; import EyesTab from "./tabs/eyes";
import NoseTab from "./tabs/nose"; import NoseTab from "./tabs/nose";
import LipsTab from "./tabs/lips"; import LipsTab from "./tabs/lips";
import EarsTab from "./tabs/ears"; import EarsTab from "./tabs/ears";
import GlassesTab from "./tabs/glasses"; import GlassesTab from "./tabs/glasses";
import OtherTab from "./tabs/other"; import OtherTab from "./tabs/other";
import MiscTab from "./tabs/misc"; import MiscTab from "./tabs/misc";
interface Props { interface Props {
instructions: React.RefObject<SwitchMiiInstructions>; instructions: React.RefObject<SwitchMiiInstructions>;
} }
type Tab = "head" | "hair" | "eyebrows" | "eyes" | "nose" | "lips" | "ears" | "glasses" | "other" | "misc"; type Tab = "head" | "hair" | "eyebrows" | "eyes" | "nose" | "lips" | "ears" | "glasses" | "other" | "misc";
export const TAB_ICONS: Record<Tab, string> = { export const TAB_ICONS: Record<Tab, string> = {
head: "mingcute:head-fill", head: "mingcute:head-fill",
hair: "mingcute:hair-fill", hair: "mingcute:hair-fill",
eyebrows: "material-symbols:eyebrow", eyebrows: "material-symbols:eyebrow",
eyes: "mdi:eye", eyes: "mdi:eye",
nose: "mingcute:nose-fill", nose: "mingcute:nose-fill",
lips: "material-symbols-light:lips", lips: "material-symbols-light:lips",
ears: "ion:ear", ears: "ion:ear",
glasses: "solar:glasses-bold", glasses: "solar:glasses-bold",
other: "mdi:sparkles", other: "mdi:sparkles",
misc: "material-symbols:settings", misc: "material-symbols:settings",
}; };
export const TAB_COMPONENTS: Record<Tab, React.ComponentType<any>> = { export const TAB_COMPONENTS: Record<Tab, React.ComponentType<any>> = {
head: HeadTab, head: HeadTab,
hair: HairTab, hair: HairTab,
eyebrows: EyebrowsTab, eyebrows: EyebrowsTab,
eyes: EyesTab, eyes: EyesTab,
nose: NoseTab, nose: NoseTab,
lips: LipsTab, lips: LipsTab,
ears: EarsTab, ears: EarsTab,
glasses: GlassesTab, glasses: GlassesTab,
other: OtherTab, other: OtherTab,
misc: MiscTab, misc: MiscTab,
}; };
export default function MiiEditor({ instructions }: Props) { export default function MiiEditor({ instructions }: Props) {
const [tab, setTab] = useState<Tab>("head"); const [tab, setTab] = useState<Tab>("head");
return ( return (
<> <>
<div className="w-full h-91 flex flex-col sm:flex-row bg-orange-100 border-2 border-orange-200 rounded-xl overflow-hidden"> <div className="w-full h-91 flex flex-col sm:flex-row bg-orange-100 border-2 border-orange-200 rounded-xl overflow-hidden">
<div className="w-full flex flex-row sm:flex-col max-sm:max-h-9 sm:max-w-9"> <div className="w-full flex flex-row sm:flex-col max-sm:max-h-9 sm:max-w-9">
{(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => ( {(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => (
<button <button
key={t} key={t}
type="button" type="button"
onClick={() => setTab(t)} onClick={() => setTab(t)}
className={`size-full aspect-square flex justify-center items-center text-[1.35rem] cursor-pointer bg-orange-200 hover:bg-orange-300 transition-colors duration-75 ${tab === t ? "bg-orange-100!" : ""}`} className={`size-full aspect-square flex justify-center items-center text-[1.35rem] cursor-pointer bg-orange-200 hover:bg-orange-300 transition-colors duration-75 ${tab === t ? "bg-orange-100!" : ""}`}
> >
{/* ml because of border on left causing icons to look miscentered */} {/* ml because of border on left causing icons to look miscentered */}
<Icon icon={TAB_ICONS[t]} className="-ml-0.5" /> <Icon icon={TAB_ICONS[t]} className="-ml-0.5" />
</button> </button>
))} ))}
</div> </div>
{/* Keep all tabs loaded to avoid flickering */} {/* Keep all tabs loaded to avoid flickering */}
{(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => { {(Object.keys(TAB_COMPONENTS) as Tab[]).map((t) => {
const TabComponent = TAB_COMPONENTS[t]; const TabComponent = TAB_COMPONENTS[t];
return ( return (
<div key={t} className={t === tab ? "grow relative p-3" : "hidden"}> <div key={t} className={t === tab ? "grow relative p-3 min-h-0" : "hidden"}>
<TabComponent instructions={instructions} /> <TabComponent instructions={instructions} />
</div> </div>
); );
})} })}
</div> </div>
</> </>
); );
} }

View file

@ -1,213 +1,213 @@
import { useState } from "react"; import { useState } from "react";
import type { MiiGender, SwitchMiiInstructions } from "@tomodachi-share/shared"; import type { MiiGender, SwitchMiiInstructions } from "@tomodachi-share/shared";
import EnhancedSlider from "../enhanced-slider"; import EnhancedSlider from "../enhanced-slider";
import DatingPreferencesViewer from "../../../mii/dating-preferences"; import DatingPreferencesViewer from "../../../mii/dating-preferences";
import VoiceViewer from "../../../mii/voice-viewer"; import VoiceViewer from "../../../mii/voice-viewer";
import PersonalityViewer from "../../../mii/personality-viewer"; import PersonalityViewer from "../../../mii/personality-viewer";
interface Props { interface Props {
instructions: React.RefObject<SwitchMiiInstructions>; instructions: React.RefObject<SwitchMiiInstructions>;
} }
export default function MiscTab({ instructions }: Props) { export default function MiscTab({ instructions }: Props) {
const [height, setHeight] = useState(instructions.current.height ?? 64); const [height, setHeight] = useState(instructions.current.height ?? 64);
const [weight, setWeight] = useState(instructions.current.weight ?? 64); const [weight, setWeight] = useState(instructions.current.weight ?? 64);
const [datingPreferences, setDatingPreferences] = useState<MiiGender[]>(instructions.current.datingPreferences ?? []); const [datingPreferences, setDatingPreferences] = useState<MiiGender[]>(instructions.current.datingPreferences ?? []);
const [voice, setVoice] = useState({ const [voice, setVoice] = useState({
speed: instructions.current.voice.speed ?? 25, speed: instructions.current.voice.speed ?? 25,
pitch: instructions.current.voice.pitch ?? 25, pitch: instructions.current.voice.pitch ?? 25,
depth: instructions.current.voice.depth ?? 25, depth: instructions.current.voice.depth ?? 25,
delivery: instructions.current.voice.delivery ?? 25, delivery: instructions.current.voice.delivery ?? 25,
tone: instructions.current.voice.tone ?? 0, tone: instructions.current.voice.tone ?? 0,
}); });
const [birthday, setBirthday] = useState({ const [birthday, setBirthday] = useState({
day: instructions.current.birthday.day ?? (null as number | null), day: instructions.current.birthday.day ?? (null as number | null),
month: instructions.current.birthday.month ?? (null as number | null), month: instructions.current.birthday.month ?? (null as number | null),
age: instructions.current.birthday.age ?? (null as number | null), age: instructions.current.birthday.age ?? (null as number | null),
dontAge: instructions.current.birthday.dontAge, dontAge: instructions.current.birthday.dontAge,
}); });
const [personality, setPersonality] = useState({ const [personality, setPersonality] = useState({
movement: instructions.current.personality.movement ?? -1, movement: instructions.current.personality.movement ?? -1,
speech: instructions.current.personality.speech ?? -1, speech: instructions.current.personality.speech ?? -1,
energy: instructions.current.personality.energy ?? -1, energy: instructions.current.personality.energy ?? -1,
thinking: instructions.current.personality.thinking ?? -1, thinking: instructions.current.personality.thinking ?? -1,
overall: instructions.current.personality.overall ?? -1, overall: instructions.current.personality.overall ?? -1,
}); });
return ( return (
<> <>
<h1 className="font-bold text-xl">Misc</h1> <h1 className="font-bold text-xl">Misc</h1>
<div className="grow h-full overflow-y-auto pb-3"> <div className="grow h-full overflow-y-auto pb-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Body</span> <span>Body</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<EnhancedSlider <EnhancedSlider
label="Height" label="Height"
value={height} value={height}
onChange={(v) => { onChange={(v) => {
setHeight(v); setHeight(v);
instructions.current.height = v; instructions.current.height = v;
}} }}
min={0} min={0}
max={128} max={128}
mid={64} mid={64}
/> />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<EnhancedSlider <EnhancedSlider
label="Weight" label="Weight"
value={weight} value={weight}
onChange={(v) => { onChange={(v) => {
setWeight(v); setWeight(v);
instructions.current.weight = v; instructions.current.weight = v;
}} }}
min={0} min={0}
max={128} max={128}
mid={64} mid={64}
/> />
</div> </div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-1.5 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-1.5 mb-2">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Dating Preferences</span> <span>Dating Preferences</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<DatingPreferencesViewer <DatingPreferencesViewer
data={datingPreferences} data={datingPreferences}
onChecked={(e, gender) => { onChecked={(e, gender) => {
setDatingPreferences((prev) => { setDatingPreferences((prev) => {
const updated = e.target.checked ? (prev.includes(gender) ? prev : [...prev, gender]) : prev.filter((p) => p !== gender); const updated = e.target.checked ? (prev.includes(gender) ? prev : [...prev, gender]) : prev.filter((p) => p !== gender);
instructions.current.datingPreferences = updated; instructions.current.datingPreferences = updated;
return updated; return updated;
}); });
}} }}
/> />
</div> </div>
</div> </div>
<div> <div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Voice</span> <span>Voice</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<VoiceViewer <VoiceViewer
data={voice} data={voice}
onChange={(v, label) => { onChange={(v, label) => {
setVoice((p) => ({ ...p, [label]: v })); setVoice((p) => ({ ...p, [label]: v }));
instructions.current.voice[label as keyof typeof voice] = v; instructions.current.voice[label as keyof typeof voice] = v;
}} }}
onClickTone={(i) => { onClickTone={(i) => {
setVoice((p) => ({ ...p, tone: i })); setVoice((p) => ({ ...p, tone: i }));
instructions.current.voice.tone = i; instructions.current.voice.tone = i;
}} }}
/> />
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-1.5 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-1.5 mb-2">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Birthday</span> <span>Birthday</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<div> <div>
<label htmlFor="day" className="text-xs"> <label htmlFor="day" className="text-xs">
Day Day
</label> </label>
<input <input
type="number" type="number"
id="day" id="day"
min={1} min={1}
max={31} max={31}
className="pill input text-sm py-1! px-3! w-full" className="pill input text-sm py-1! px-3! w-full"
value={birthday.day ?? undefined} value={birthday.day ?? undefined}
onChange={(e) => { onChange={(e) => {
setBirthday((p) => ({ ...p, day: e.target.valueAsNumber })); setBirthday((p) => ({ ...p, day: e.target.valueAsNumber }));
instructions.current.birthday.day = e.target.valueAsNumber; instructions.current.birthday.day = e.target.valueAsNumber;
}} }}
/> />
</div> </div>
<div> <div>
<label htmlFor="month" className="text-xs"> <label htmlFor="month" className="text-xs">
Month Month
</label> </label>
<input <input
type="number" type="number"
id="month" id="month"
min={1} min={1}
max={12} max={12}
className="pill input text-sm py-1! px-3! w-full" className="pill input text-sm py-1! px-3! w-full"
value={birthday.month ?? undefined} value={birthday.month ?? undefined}
onChange={(e) => { onChange={(e) => {
setBirthday((p) => ({ ...p, month: e.target.valueAsNumber })); setBirthday((p) => ({ ...p, month: e.target.valueAsNumber }));
instructions.current.birthday.month = e.target.valueAsNumber; instructions.current.birthday.month = e.target.valueAsNumber;
}} }}
/> />
</div> </div>
<div> <div>
<label htmlFor="age" className="text-xs"> <label htmlFor="age" className="text-xs">
Age Age
</label> </label>
<input <input
type="number" type="number"
id="age" id="age"
min={1} min={1}
max={1000} max={1000}
className="pill input text-sm py-1! px-3! w-full" className="pill input text-sm py-1! px-3! w-full"
value={birthday.age ?? undefined} value={birthday.age ?? undefined}
onChange={(e) => { onChange={(e) => {
setBirthday((p) => ({ ...p, age: e.target.valueAsNumber })); setBirthday((p) => ({ ...p, age: e.target.valueAsNumber }));
instructions.current.birthday.age = e.target.valueAsNumber; instructions.current.birthday.age = e.target.valueAsNumber;
}} }}
/> />
</div> </div>
<div className="flex gap-1.5 col-span-2"> <div className="flex gap-1.5 col-span-2">
<input <input
type="checkbox" type="checkbox"
id="dontAge" id="dontAge"
className="checkbox" className="checkbox"
checked={birthday.dontAge} checked={birthday.dontAge}
onChange={(e) => { onChange={(e) => {
setBirthday((p) => ({ ...p, dontAge: e.target.checked })); setBirthday((p) => ({ ...p, dontAge: e.target.checked }));
instructions.current.birthday.dontAge = e.target.checked; instructions.current.birthday.dontAge = e.target.checked;
}} }}
/> />
<label htmlFor="dontAge" className="text-sm select-none"> <label htmlFor="dontAge" className="text-sm select-none">
Don't Age Don't Age
</label> </label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-2 mb-2"> <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-2 mb-2">
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
<span>Personality</span> <span>Personality</span>
<hr className="grow border-zinc-300" /> <hr className="grow border-zinc-300" />
</div> </div>
<PersonalityViewer <PersonalityViewer
data={personality} data={personality}
onClick={(key, i) => { onClick={(key, i) => {
setPersonality((p) => { setPersonality((p) => {
const updated = { ...p, [key]: i }; const updated = { ...p, [key]: i };
instructions.current.personality = updated; instructions.current.personality = updated;
return updated; return updated;
}); });
}} }}
/> />
</div> </div>
</> </>
); );
} }