feat: adding right content to text field

This commit is contained in:
Chubby Granny Chaser 2024-06-12 18:19:38 +01:00
parent 55d1bfb34d
commit 50665b4472
No known key found for this signature in database
10 changed files with 123 additions and 118 deletions

View File

@ -40,6 +40,11 @@ export const textField = recipe({
backgroundColor: vars.color.background, backgroundColor: vars.color.background,
}, },
}, },
state: {
error: {
borderColor: vars.color.danger,
},
},
}, },
}); });
@ -73,3 +78,8 @@ export const togglePasswordButton = style({
color: vars.color.muted, color: vars.color.muted,
padding: `${SPACING_UNIT}px`, padding: `${SPACING_UNIT}px`,
}); });
export const textFieldWrapper = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View File

@ -1,4 +1,4 @@
import { useId, useMemo, useState } from "react"; import React, { useId, useMemo, useState } from "react";
import type { RecipeVariants } from "@vanilla-extract/recipes"; import type { RecipeVariants } from "@vanilla-extract/recipes";
import * as styles from "./text-field.css"; import * as styles from "./text-field.css";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react"; import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
@ -20,6 +20,8 @@ export interface TextFieldProps
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
>; >;
rightContent?: React.ReactNode | null;
state?: NonNullable<RecipeVariants<typeof styles.textField>>["state"];
} }
export function TextField({ export function TextField({
@ -28,6 +30,8 @@ export function TextField({
hint, hint,
textFieldProps, textFieldProps,
containerProps, containerProps,
rightContent = null,
state,
...props ...props
}: TextFieldProps) { }: TextFieldProps) {
const id = useId(); const id = useId();
@ -48,33 +52,37 @@ export function TextField({
<div className={styles.textFieldContainer} {...containerProps}> <div className={styles.textFieldContainer} {...containerProps}>
{label && <label htmlFor={id}>{label}</label>} {label && <label htmlFor={id}>{label}</label>}
<div <div className={styles.textFieldWrapper}>
className={styles.textField({ focused: isFocused, theme })} <div
{...textFieldProps} className={styles.textField({ focused: isFocused, theme, state })}
> {...textFieldProps}
<input >
id={id} <input
className={styles.textFieldInput({ readOnly: props.readOnly })} id={id}
onFocus={() => setIsFocused(true)} className={styles.textFieldInput({ readOnly: props.readOnly })}
onBlur={() => setIsFocused(false)} onFocus={() => setIsFocused(true)}
{...props} onBlur={() => setIsFocused(false)}
type={inputType} {...props}
/> type={inputType}
/>
{showPasswordToggleButton && ( {showPasswordToggleButton && (
<button <button
type="button" type="button"
className={styles.togglePasswordButton} className={styles.togglePasswordButton}
onClick={() => setIsPasswordVisible(!isPasswordVisible)} onClick={() => setIsPasswordVisible(!isPasswordVisible)}
aria-label={t("toggle_password_visibility")} aria-label={t("toggle_password_visibility")}
> >
{isPasswordVisible ? ( {isPasswordVisible ? (
<EyeClosedIcon size={16} /> <EyeClosedIcon size={16} />
) : ( ) : (
<EyeIcon size={16} /> <EyeIcon size={16} />
)} )}
</button> </button>
)} )}
</div>
{rightContent}
</div> </div>
{hint && <small>{hint}</small>} {hint && <small>{hint}</small>}

View File

@ -13,7 +13,10 @@ import * as styles from "./game-details.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
const HERO_ANIMATION_THRESHOLD = 25;
export function GameDetailsContent() { export function GameDetailsContent() {
const heroRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [isHeaderStuck, setIsHeaderStuck] = useState(false); const [isHeaderStuck, setIsHeaderStuck] = useState(false);
@ -42,14 +45,19 @@ export function GameDetailsContent() {
}, [objectID]); }, [objectID]);
const onScroll: React.UIEventHandler<HTMLElement> = (event) => { const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
const scrollY = (event.target as HTMLDivElement).scrollTop; const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
const opacity = Math.max(0, 1 - scrollY / styles.HERO_HEIGHT);
if (scrollY >= styles.HERO_HEIGHT && !isHeaderStuck) { const scrollY = (event.target as HTMLDivElement).scrollTop;
const opacity = Math.max(
0,
1 - scrollY / (heroHeight - HERO_ANIMATION_THRESHOLD)
);
if (scrollY >= heroHeight && !isHeaderStuck) {
setIsHeaderStuck(true); setIsHeaderStuck(true);
} }
if (scrollY <= styles.HERO_HEIGHT && isHeaderStuck) { if (scrollY <= heroHeight && isHeaderStuck) {
setIsHeaderStuck(false); setIsHeaderStuck(false);
} }
@ -70,7 +78,7 @@ export function GameDetailsContent() {
onScroll={onScroll} onScroll={onScroll}
className={styles.container} className={styles.container}
> >
<div className={styles.hero}> <div ref={heroRef} className={styles.hero}>
<div <div
style={{ style={{
backgroundColor: gameColor, backgroundColor: gameColor,

View File

@ -22,7 +22,7 @@ export const panel = recipe({
variants: { variants: {
stuck: { stuck: {
true: { true: {
boxShadow: "0px 0px 15px 0px #000000", boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
}, },
}, },
}, },

View File

@ -105,22 +105,23 @@ export function GameOptionsModal({
{t("executable_section_description")} {t("executable_section_description")}
</h4> </h4>
</div> </div>
<div className={styles.gameOptionRow}>
<TextField <TextField
value={game.executablePath || ""} value={game.executablePath || ""}
readOnly readOnly
theme="dark" theme="dark"
disabled disabled
placeholder={t("no_executable_selected")} placeholder={t("no_executable_selected")}
/> rightContent={
<Button <Button
type="button" type="button"
theme="outline" theme="outline"
onClick={handleChangeExecutableLocation} onClick={handleChangeExecutableLocation}
> >
{t("select_executable")} {t("select_executable")}
</Button> </Button>
</div> }
/>
{game.executablePath && ( {game.executablePath && (
<div className={styles.gameOptionRow}> <div className={styles.gameOptionRow}>

View File

@ -1,9 +1,8 @@
import { Button, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./settings-download-sources.css"; import { Button, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
interface AddDownloadSourceModalProps { interface AddDownloadSourceModalProps {
visible: boolean; visible: boolean;
@ -64,24 +63,23 @@ export function AddDownloadSourceModal({
minWidth: "500px", minWidth: "500px",
}} }}
> >
<div className={styles.downloadSourceField}> <TextField
<TextField label={t("download_source_url")}
label={t("download_source_url")} placeholder="Insert a valid JSON url"
placeholder="Insert a valid JSON url" value={value}
value={value} onChange={(e) => setValue(e.target.value)}
onChange={(e) => setValue(e.target.value)} rightContent={
/> <Button
type="button"
<Button theme="outline"
type="button" style={{ alignSelf: "flex-end" }}
theme="outline" onClick={handleValidateDownloadSource}
style={{ alignSelf: "flex-end" }} disabled={isLoading || !value}
onClick={handleValidateDownloadSource} >
disabled={isLoading || !value} {t("validate_download_source")}
> </Button>
{t("validate_download_source")} }
</Button> />
</div>
{validationResult && ( {validationResult && (
<div <div

View File

@ -2,11 +2,6 @@ import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css"; import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes"; import { recipe } from "@vanilla-extract/recipes";
export const downloadSourceField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const downloadSources = style({ export const downloadSources = style({
padding: "0", padding: "0",
margin: "0", margin: "0",

View File

@ -137,25 +137,23 @@ export function SettingsDownloadSources() {
</div> </div>
</div> </div>
<div className={styles.downloadSourceField}> <TextField
<TextField label={t("download_source_url")}
label={t("download_source_url")} value={downloadSource.url}
value={downloadSource.url} readOnly
readOnly theme="dark"
theme="dark" disabled
disabled rightContent={
/> <Button
type="button"
<Button theme="outline"
type="button" onClick={() => handleRemoveSource(downloadSource.id)}
theme="outline" >
style={{ alignSelf: "flex-end" }} <NoEntryIcon />
onClick={() => handleRemoveSource(downloadSource.id)} {t("remove_download_source")}
> </Button>
<NoEntryIcon /> }
{t("remove_download_source")} />
</Button>
</div>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -1,7 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const downloadsPathField = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});

View File

@ -8,7 +8,6 @@ import {
SelectField, SelectField,
} from "@renderer/components"; } from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./settings-general.css";
import type { UserPreferences } from "@types"; import type { UserPreferences } from "@types";
import { useAppSelector } from "@renderer/hooks"; import { useAppSelector } from "@renderer/hooks";
@ -113,22 +112,17 @@ export function SettingsGeneral({
return ( return (
<> <>
<div className={styles.downloadsPathField}> <TextField
<TextField label={t("downloads_path")}
label={t("downloads_path")} value={form.downloadsPath}
value={form.downloadsPath} readOnly
readOnly disabled
disabled rightContent={
/> <Button theme="outline" onClick={handleChooseDownloadsPath}>
{t("change")}
<Button </Button>
style={{ alignSelf: "flex-end" }} }
theme="outline" />
onClick={handleChooseDownloadsPath}
>
{t("change")}
</Button>
</div>
<SelectField <SelectField
label={t("language")} label={t("language")}