mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
feat: adding right content to text field
This commit is contained in:
parent
55d1bfb34d
commit
50665b4472
@ -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`,
|
||||||
|
});
|
||||||
|
@ -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>}
|
||||||
|
@ -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,
|
||||||
|
@ -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)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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}>
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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`,
|
|
||||||
});
|
|
@ -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")}
|
||||||
|
Loading…
Reference in New Issue
Block a user