diff --git a/src/renderer/src/components/text-field/text-field.css.ts b/src/renderer/src/components/text-field/text-field.css.ts index 09f64498..4e986378 100644 --- a/src/renderer/src/components/text-field/text-field.css.ts +++ b/src/renderer/src/components/text-field/text-field.css.ts @@ -40,6 +40,11 @@ export const textField = recipe({ backgroundColor: vars.color.background, }, }, + state: { + error: { + borderColor: vars.color.danger, + }, + }, }, }); @@ -73,3 +78,8 @@ export const togglePasswordButton = style({ color: vars.color.muted, padding: `${SPACING_UNIT}px`, }); + +export const textFieldWrapper = style({ + display: "flex", + gap: `${SPACING_UNIT}px`, +}); diff --git a/src/renderer/src/components/text-field/text-field.tsx b/src/renderer/src/components/text-field/text-field.tsx index c7fe501e..08562cc3 100644 --- a/src/renderer/src/components/text-field/text-field.tsx +++ b/src/renderer/src/components/text-field/text-field.tsx @@ -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 * as styles from "./text-field.css"; import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react"; @@ -20,6 +20,8 @@ export interface TextFieldProps React.HTMLAttributes, HTMLDivElement >; + rightContent?: React.ReactNode | null; + state?: NonNullable>["state"]; } export function TextField({ @@ -28,6 +30,8 @@ export function TextField({ hint, textFieldProps, containerProps, + rightContent = null, + state, ...props }: TextFieldProps) { const id = useId(); @@ -48,33 +52,37 @@ export function TextField({
{label && } -
- setIsFocused(true)} - onBlur={() => setIsFocused(false)} - {...props} - type={inputType} - /> +
+
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + {...props} + type={inputType} + /> - {showPasswordToggleButton && ( - - )} + {showPasswordToggleButton && ( + + )} +
+ + {rightContent}
{hint && {hint}} diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index f6cb60e5..a4965ca0 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -13,7 +13,10 @@ import * as styles from "./game-details.css"; import { useTranslation } from "react-i18next"; import { gameDetailsContext } from "@renderer/context"; +const HERO_ANIMATION_THRESHOLD = 25; + export function GameDetailsContent() { + const heroRef = useRef(null); const containerRef = useRef(null); const [isHeaderStuck, setIsHeaderStuck] = useState(false); @@ -42,14 +45,19 @@ export function GameDetailsContent() { }, [objectID]); const onScroll: React.UIEventHandler = (event) => { - const scrollY = (event.target as HTMLDivElement).scrollTop; - const opacity = Math.max(0, 1 - scrollY / styles.HERO_HEIGHT); + const heroHeight = heroRef.current?.clientHeight ?? 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); } - if (scrollY <= styles.HERO_HEIGHT && isHeaderStuck) { + if (scrollY <= heroHeight && isHeaderStuck) { setIsHeaderStuck(false); } @@ -70,7 +78,7 @@ export function GameDetailsContent() { onScroll={onScroll} className={styles.container} > -
+
-
- - -
+ + + {t("select_executable")} + + } + /> {game.executablePath && (
diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 422c1419..78c3dbc6 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -1,9 +1,8 @@ -import { Button, Modal, TextField } from "@renderer/components"; -import { SPACING_UNIT } from "@renderer/theme.css"; import { useEffect, useState } from "react"; 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 { visible: boolean; @@ -64,24 +63,23 @@ export function AddDownloadSourceModal({ minWidth: "500px", }} > -
- setValue(e.target.value)} - /> - - -
+ setValue(e.target.value)} + rightContent={ + + } + /> {validationResult && (
-
- - - -
+ handleRemoveSource(downloadSource.id)} + > + + {t("remove_download_source")} + + } + /> ))} diff --git a/src/renderer/src/pages/settings/settings-general.css.ts b/src/renderer/src/pages/settings/settings-general.css.ts deleted file mode 100644 index d403139f..00000000 --- a/src/renderer/src/pages/settings/settings-general.css.ts +++ /dev/null @@ -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`, -}); diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index e2f1f1d7..fb9e459e 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -8,7 +8,6 @@ import { SelectField, } from "@renderer/components"; import { useTranslation } from "react-i18next"; -import * as styles from "./settings-general.css"; import type { UserPreferences } from "@types"; import { useAppSelector } from "@renderer/hooks"; @@ -113,22 +112,17 @@ export function SettingsGeneral({ return ( <> -
- - - -
+ + {t("change")} + + } + />