mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +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,
|
||||
},
|
||||
},
|
||||
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`,
|
||||
});
|
||||
|
@ -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>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
rightContent?: React.ReactNode | null;
|
||||
state?: NonNullable<RecipeVariants<typeof styles.textField>>["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({
|
||||
<div className={styles.textFieldContainer} {...containerProps}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
||||
<div
|
||||
className={styles.textField({ focused: isFocused, theme })}
|
||||
{...textFieldProps}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
{...props}
|
||||
type={inputType}
|
||||
/>
|
||||
<div className={styles.textFieldWrapper}>
|
||||
<div
|
||||
className={styles.textField({ focused: isFocused, theme, state })}
|
||||
{...textFieldProps}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
{...props}
|
||||
type={inputType}
|
||||
/>
|
||||
|
||||
{showPasswordToggleButton && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.togglePasswordButton}
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
aria-label={t("toggle_password_visibility")}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<EyeClosedIcon size={16} />
|
||||
) : (
|
||||
<EyeIcon size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showPasswordToggleButton && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.togglePasswordButton}
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
aria-label={t("toggle_password_visibility")}
|
||||
>
|
||||
{isPasswordVisible ? (
|
||||
<EyeClosedIcon size={16} />
|
||||
) : (
|
||||
<EyeIcon size={16} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rightContent}
|
||||
</div>
|
||||
|
||||
{hint && <small>{hint}</small>}
|
||||
|
@ -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<HTMLDivElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isHeaderStuck, setIsHeaderStuck] = useState(false);
|
||||
|
||||
@ -42,14 +45,19 @@ export function GameDetailsContent() {
|
||||
}, [objectID]);
|
||||
|
||||
const onScroll: React.UIEventHandler<HTMLElement> = (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}
|
||||
>
|
||||
<div className={styles.hero}>
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: gameColor,
|
||||
|
@ -22,7 +22,7 @@ export const panel = recipe({
|
||||
variants: {
|
||||
stuck: {
|
||||
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")}
|
||||
</h4>
|
||||
</div>
|
||||
<div className={styles.gameOptionRow}>
|
||||
<TextField
|
||||
value={game.executablePath || ""}
|
||||
readOnly
|
||||
theme="dark"
|
||||
disabled
|
||||
placeholder={t("no_executable_selected")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleChangeExecutableLocation}
|
||||
>
|
||||
{t("select_executable")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
value={game.executablePath || ""}
|
||||
readOnly
|
||||
theme="dark"
|
||||
disabled
|
||||
placeholder={t("no_executable_selected")}
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={handleChangeExecutableLocation}
|
||||
>
|
||||
{t("select_executable")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{game.executablePath && (
|
||||
<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 { 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",
|
||||
}}
|
||||
>
|
||||
<div className={styles.downloadSourceField}>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
placeholder="Insert a valid JSON url"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={handleValidateDownloadSource}
|
||||
disabled={isLoading || !value}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
</Button>
|
||||
</div>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
placeholder="Insert a valid JSON url"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={handleValidateDownloadSource}
|
||||
disabled={isLoading || !value}
|
||||
>
|
||||
{t("validate_download_source")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{validationResult && (
|
||||
<div
|
||||
|
@ -2,11 +2,6 @@ import { style } from "@vanilla-extract/css";
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
import { recipe } from "@vanilla-extract/recipes";
|
||||
|
||||
export const downloadSourceField = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadSources = style({
|
||||
padding: "0",
|
||||
margin: "0",
|
||||
|
@ -137,25 +137,23 @@ export function SettingsDownloadSources() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.downloadSourceField}>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
value={downloadSource.url}
|
||||
readOnly
|
||||
theme="dark"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||
>
|
||||
<NoEntryIcon />
|
||||
{t("remove_download_source")}
|
||||
</Button>
|
||||
</div>
|
||||
<TextField
|
||||
label={t("download_source_url")}
|
||||
value={downloadSource.url}
|
||||
readOnly
|
||||
theme="dark"
|
||||
disabled
|
||||
rightContent={
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||
>
|
||||
<NoEntryIcon />
|
||||
{t("remove_download_source")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</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,
|
||||
} 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 (
|
||||
<>
|
||||
<div className={styles.downloadsPathField}>
|
||||
<TextField
|
||||
label={t("downloads_path")}
|
||||
value={form.downloadsPath}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Button
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
theme="outline"
|
||||
onClick={handleChooseDownloadsPath}
|
||||
>
|
||||
{t("change")}
|
||||
</Button>
|
||||
</div>
|
||||
<TextField
|
||||
label={t("downloads_path")}
|
||||
value={form.downloadsPath}
|
||||
readOnly
|
||||
disabled
|
||||
rightContent={
|
||||
<Button theme="outline" onClick={handleChooseDownloadsPath}>
|
||||
{t("change")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
label={t("language")}
|
||||
|
Loading…
Reference in New Issue
Block a user