mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
feat: merge pull request #23 from LandonAndEmma/main
Feat: Add Better Sliders
This commit is contained in:
commit
fb4cad1296
5 changed files with 143 additions and 116 deletions
|
|
@ -119,12 +119,9 @@ input[type="range"]::-moz-range-track {
|
|||
}
|
||||
|
||||
/* Thumb */
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
@apply appearance-none size-4 bg-orange-300 border-2 border-orange-400 rounded-full shadow-md transition -mt-1.5;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb,
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
@apply size-3.5 bg-orange-300 border-2 border-orange-400 rounded-full shadow-md transition;
|
||||
@apply appearance-none size-4.5 bg-orange-400 border-2 border-orange-600 rounded-full shadow-md transition;
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import VoiceViewer from "./voice-viewer";
|
|||
import PersonalityViewer from "./personality-viewer";
|
||||
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { COLORS } from "@/lib/switch";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -198,37 +197,35 @@ export default function MiiInstructions({ instructions }: Props) {
|
|||
<div className="p-3 text-sm border-l-4 border-amber-400 bg-amber-100/50 rounded-r-lg py-2.5 text-amber-950 w-max">
|
||||
<h3 className="font-semibold text-xl text-amber-800 mb-1">Misc</h3>
|
||||
|
||||
{height && (
|
||||
<div className="flex mb-1">
|
||||
<label htmlFor="height" className="w-16">
|
||||
Height
|
||||
</label>
|
||||
<div className="relative h-5 flex justify-center items-center">
|
||||
<input id="height" type="range" min={0} max={128} step={1} disabled value={height} />
|
||||
<div className="absolute h-4 w-1.5 rounded bg-orange-300 z-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{weight && (
|
||||
<div className="flex">
|
||||
<label htmlFor="weight" className="w-16">
|
||||
Weight
|
||||
</label>
|
||||
<div className="relative h-5 flex justify-center items-center">
|
||||
<input id="weight" type="range" min={0} max={128} step={1} disabled value={weight} />
|
||||
<div className="absolute h-4 w-1.5 rounded bg-orange-300 z-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
{not(height) && <TableCell label="Height">{height === 64 ? "0" : height! > 64 ? `+${height! - 64}` : `${height! - 64}`}</TableCell>}
|
||||
{not(weight) && <TableCell label="Weight">{weight === 64 ? "0" : weight! > 64 ? `+${weight! - 64}` : `${weight! - 64}`}</TableCell>}
|
||||
</tbody>
|
||||
</table>
|
||||
{birthday && (
|
||||
<div className="pl-2 not-nth-2:mt-4">
|
||||
<h4 className="font-semibold text-xl text-amber-800 mb-1">Birthday</h4>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
{birthday.day && <TableCell label="Day">{birthday.day}</TableCell>}
|
||||
{birthday.month && <TableCell label="Month">{birthday.month}</TableCell>}
|
||||
{birthday.age && <TableCell label="Age">{birthday.age}</TableCell>}
|
||||
{birthday.dontAge && <TableCell label="Don't Age">{birthday.dontAge ? "Yes" : "No"}</TableCell>}
|
||||
{not(birthday.day) && <TableCell label="Day">{birthday.day}</TableCell>}
|
||||
{not(birthday.month) && <TableCell label="Month">{birthday.month}</TableCell>}
|
||||
{not(birthday.age) && <TableCell label="Age">{birthday.age}</TableCell>}
|
||||
{not(birthday.dontAge) && <TableCell label="Don't Age">{birthday.dontAge ? "Yes" : "No"}</TableCell>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{voice && (
|
||||
<div className="pl-2 not-nth-2:mt-4">
|
||||
<h4 className="font-semibold text-xl text-amber-800 mb-1">Voice</h4>
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
{not(voice.speed) && <TableCell label="Speed">{voice.speed}</TableCell>}
|
||||
{not(voice.pitch) && <TableCell label="Pitch">{voice.pitch}</TableCell>}
|
||||
{not(voice.depth) && <TableCell label="Depth">{voice.depth}</TableCell>}
|
||||
{not(voice.delivery) && <TableCell label="Delivery">{voice.delivery}</TableCell>}
|
||||
{not(voice.tone) && <TableCell label="Tone">{voice.tone}</TableCell>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -241,14 +238,6 @@ export default function MiiInstructions({ instructions }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{voice && (
|
||||
<div className="pl-2 not-nth-2:mt-4">
|
||||
<h4 className="font-semibold text-xl text-amber-800 mb-1">Voice</h4>
|
||||
<div className="w-min">
|
||||
<VoiceViewer data={voice} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{personality && (
|
||||
<div className="pl-2 not-nth-2:mt-4">
|
||||
<h4 className="font-semibold text-xl text-amber-800 mb-1">Personality</h4>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
import { ChangeEvent } from "react";
|
||||
import EnhancedSlider from "@/components/submit-form/mii-editor/enhanced-slider";
|
||||
|
||||
interface Props {
|
||||
data: SwitchMiiInstructions["voice"];
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement, HTMLInputElement>, label: string) => void;
|
||||
onClickTone?: (i: number) => void;
|
||||
onChange: (value: number, label: string) => void;
|
||||
onClickTone: (i: number) => void;
|
||||
}
|
||||
|
||||
const VOICE_SETTINGS: string[] = ["Speed", "Pitch", "Depth", "Delivery"];
|
||||
const VOICE_SETTINGS = ["Speed", "Pitch", "Depth", "Delivery"];
|
||||
|
||||
export default function VoiceViewer({ data, onChange, onClickTone }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{VOICE_SETTINGS.map((label) => (
|
||||
<div key={label} className="relative flex gap-3">
|
||||
<label htmlFor={label} className="text-sm w-14">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative h-5 flex justify-center items-center">
|
||||
<input
|
||||
type="range"
|
||||
name={label}
|
||||
className="grow z-10"
|
||||
min={0}
|
||||
max={50}
|
||||
step={1}
|
||||
value={data[label.toLowerCase() as keyof typeof data] ?? 25}
|
||||
disabled={!onChange}
|
||||
onChange={(e) => {
|
||||
if (onChange) onChange(e, label.toLowerCase());
|
||||
}}
|
||||
/>
|
||||
<div className="absolute h-4 w-1.5 rounded bg-orange-300 z-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col">
|
||||
{VOICE_SETTINGS.map((label) => {
|
||||
const value = data[label.toLowerCase() as keyof typeof data] ?? 25;
|
||||
return <EnhancedSlider key={label} label={label} value={value} onChange={(v) => onChange?.(v, label.toLowerCase())} min={0} max={50} mid={25} />;
|
||||
})}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mt-2">
|
||||
<label htmlFor="delivery" className="text-sm w-14">
|
||||
Tone
|
||||
</label>
|
||||
<div className="grid grid-cols-6 gap-1 grow">
|
||||
<div className="grid grid-cols-6 gap-1 min-w-50">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -50,7 +30,7 @@ export default function VoiceViewer({ data, onChange, onClickTone }: Props) {
|
|||
onClick={() => {
|
||||
if (onClickTone) onClickTone(i);
|
||||
}}
|
||||
className={`transition-colors duration-100 rounded-xl ${data.tone === i ? "bg-orange-400!" : ""} ${onClickTone ? "hover:bg-orange-300 cursor-pointer" : ""}`}
|
||||
className={`transition-colors duration-100 rounded-xl hover:bg-orange-300 cursor-pointer ${data.tone === i ? "bg-orange-400!" : ""}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
|
|
|
|||
78
src/components/submit-form/mii-editor/enhanced-slider.tsx
Normal file
78
src/components/submit-form/mii-editor/enhanced-slider.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface SliderProps {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
mid?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function EnhancedSlider({ label, value, onChange, min = 0, max = 128, mid = 64, step = 1, className = "" }: SliderProps) {
|
||||
const handleChange = (newValue: number) => {
|
||||
const clampedValue = Math.min(max, Math.max(min, newValue));
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
const nudge = (direction: number) => {
|
||||
const newValue = value + direction * step;
|
||||
handleChange(newValue);
|
||||
};
|
||||
|
||||
const displayValue = value - mid;
|
||||
const displayText = displayValue > 0 ? `+${displayValue}` : displayValue.toString();
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
<div className="flex justify-between items-center my-1 relative">
|
||||
<h3 className="text-sm font-semibold">{label}</h3>
|
||||
<span className="absolute left-1/2 transform -translate-x-1/2 text-xs font-bold text-orange-600 bg-orange-50 border-2 border-orange-400 px-2 py-1 rounded-full shadow-sm">
|
||||
{displayText}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => nudge(-1)}
|
||||
disabled={value <= min}
|
||||
className="bg-orange-50 border-2 border-orange-400 text-orange-400 font-bold size-7 rounded-lg cursor-pointer flex items-center justify-center shrink-0 transition-transform not-disabled:active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-orange-50"
|
||||
aria-label={`Decrease ${label}`}
|
||||
>
|
||||
<Icon icon="mdi:chevron-left" width="16" height="16" />
|
||||
</button>
|
||||
|
||||
<div className="relative flex-1 h-8 flex items-center">
|
||||
{/* Tick mark at center */}
|
||||
<div className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 w-0.5 h-3 bg-orange-400 rounded z-10 opacity-60"></div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => handleChange(e.target.valueAsNumber)}
|
||||
className="w-full px-0.5 h-2 bg-orange-200 rounded-lg appearance-none cursor-pointer focus:outline-0"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #fb923c 0%, #fb923c ${percentage}%, #fed7aa ${percentage}%, #fed7aa 100%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => nudge(1)}
|
||||
disabled={value >= max}
|
||||
className="bg-orange-50 border-2 border-orange-400 text-orange-400 font-bold size-7 rounded-lg cursor-pointer flex items-center justify-center shrink-0 transition-transform not-disabled:active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-orange-50"
|
||||
aria-label={`Increase ${label}`}
|
||||
>
|
||||
<Icon icon="mdi:chevron-right" width="16" height="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
import { useState } from "react";
|
||||
import { MiiGender } from "@prisma/client";
|
||||
|
||||
import DatingPreferencesViewer from "@/components/mii/dating-preferences";
|
||||
import VoiceViewer from "@/components/mii/voice-viewer";
|
||||
import PersonalityViewer from "@/components/mii/personality-viewer";
|
||||
|
||||
import EnhancedSlider from "@/components/submit-form/mii-editor/enhanced-slider";
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
|
||||
interface Props {
|
||||
instructions: React.RefObject<SwitchMiiInstructions>;
|
||||
}
|
||||
|
||||
export default function HeadTab({ instructions }: Props) {
|
||||
export default function MiscTab({ instructions }: Props) {
|
||||
const [height, setHeight] = useState(instructions.current.height ?? 64);
|
||||
const [weight, setWeight] = useState(instructions.current.weight ?? 64);
|
||||
const [datingPreferences, setDatingPreferences] = useState<MiiGender[]>(instructions.current.datingPreferences ?? []);
|
||||
|
|
@ -50,47 +49,31 @@ export default function HeadTab({ instructions }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="height" className="text-sm">
|
||||
Height
|
||||
</label>
|
||||
<div className="relative h-5 flex justify-center items-center">
|
||||
<input
|
||||
type="range"
|
||||
id="height"
|
||||
className="grow z-10"
|
||||
<EnhancedSlider
|
||||
label="Height"
|
||||
value={height}
|
||||
onChange={(v) => {
|
||||
setHeight(v);
|
||||
instructions.current.height = v;
|
||||
}}
|
||||
min={0}
|
||||
max={128}
|
||||
step={1}
|
||||
value={height}
|
||||
onChange={(e) => {
|
||||
setHeight(e.target.valueAsNumber);
|
||||
instructions.current.height = e.target.valueAsNumber;
|
||||
}}
|
||||
mid={64}
|
||||
/>
|
||||
<div className="absolute h-4 w-1.5 rounded bg-orange-300 z-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="weight" className="text-sm">
|
||||
Weight
|
||||
</label>
|
||||
<div className="relative h-5 flex justify-center items-center">
|
||||
<input
|
||||
type="range"
|
||||
id="weight"
|
||||
className="grow z-10"
|
||||
<EnhancedSlider
|
||||
label="Weight"
|
||||
value={weight}
|
||||
onChange={(v) => {
|
||||
setWeight(v);
|
||||
instructions.current.weight = v;
|
||||
}}
|
||||
min={0}
|
||||
max={128}
|
||||
step={1}
|
||||
value={weight}
|
||||
onChange={(e) => {
|
||||
setWeight(e.target.valueAsNumber);
|
||||
instructions.current.weight = e.target.valueAsNumber;
|
||||
}}
|
||||
mid={64}
|
||||
/>
|
||||
<div className="absolute h-4 w-1.5 rounded bg-orange-300 z-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-1.5 mb-2">
|
||||
|
|
@ -122,9 +105,9 @@ export default function HeadTab({ instructions }: Props) {
|
|||
|
||||
<VoiceViewer
|
||||
data={voice}
|
||||
onChange={(e, label) => {
|
||||
setVoice((p) => ({ ...p, [label]: e.target.valueAsNumber }));
|
||||
instructions.current.voice[label as keyof typeof voice] = e.target.valueAsNumber;
|
||||
onChange={(v, label) => {
|
||||
setVoice((p) => ({ ...p, [label]: v }));
|
||||
instructions.current.voice[label as keyof typeof voice] = v;
|
||||
}}
|
||||
onClickTone={(i) => {
|
||||
setVoice((p) => ({ ...p, tone: i }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue