feat: Pattern Matching TextField + added Minecraft settings page
This commit is contained in:
parent
af177152f0
commit
ff989fba41
4 changed files with 105 additions and 5 deletions
|
|
@ -1,8 +1,13 @@
|
||||||
.textfield {
|
.textfield {
|
||||||
@apply bg-component-bg [&:not(:focus-within)]:hover:bg-component-bg-hover focus-within:bg-component-bg-pressed disabled:bg-component-bg-disabled;
|
@apply bg-component-bg [&:not(:focus-within)]:hover:bg-component-bg-hover focus-within:bg-component-bg-pressed disabled:bg-component-bg-disabled;
|
||||||
@apply border border-gray-05 text-fg-primary fill-fg-primary;
|
@apply border border-gray-05 text-fg-primary fill-fg-primary;
|
||||||
@apply rounded-lg px-2 py-1 max-h-8;
|
@apply rounded-lg px-2.5 py-1.5 max-h-8;
|
||||||
@apply flex flex-row items-center justify-start gap-x-1.5;
|
@apply flex flex-row items-center justify-start gap-x-1.5;
|
||||||
|
@apply text-sm;
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
@apply border-danger;
|
||||||
|
}
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
&::selection {
|
&::selection {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,57 @@
|
||||||
import type { JSX } from 'solid-js';
|
import type { JSX } from 'solid-js';
|
||||||
import { createUniqueId, splitProps } from 'solid-js';
|
import { createEffect, createSignal, createUniqueId, splitProps } from 'solid-js';
|
||||||
import styles from './TextField.module.scss';
|
import styles from './TextField.module.scss';
|
||||||
|
|
||||||
type TextFieldProps = {
|
type TextFieldProps = {
|
||||||
iconLeft?: JSX.Element;
|
iconLeft?: JSX.Element;
|
||||||
iconRight?: JSX.Element;
|
iconRight?: JSX.Element;
|
||||||
|
inputFilter?: (value: string) => boolean;
|
||||||
|
onValidInput?: (value: string) => any;
|
||||||
|
onValidSubmit?: (value: string) => any;
|
||||||
} & JSX.InputHTMLAttributes<HTMLInputElement>;
|
} & JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
function TextField(props: TextFieldProps) {
|
function TextField(props: TextFieldProps) {
|
||||||
const [fieldProps, rest] = splitProps(props, ['iconLeft', 'iconRight']);
|
const [fieldProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'inputFilter', 'onValidInput', 'onValidSubmit']);
|
||||||
|
const [isValid, setIsValid] = createSignal(true);
|
||||||
const id = createUniqueId();
|
const id = createUniqueId();
|
||||||
|
|
||||||
|
function validate(e: Event & { currentTarget: HTMLInputElement }) {
|
||||||
|
if (!fieldProps.inputFilter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
const valid = fieldProps.inputFilter(value);
|
||||||
|
setIsValid(valid);
|
||||||
|
|
||||||
|
if (valid && fieldProps.onValidInput)
|
||||||
|
fieldProps.onValidInput(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit(e: Event & { currentTarget: HTMLInputElement }) {
|
||||||
|
if (isValid() && fieldProps.onValidSubmit)
|
||||||
|
fieldProps.onValidSubmit(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label for={id} class={styles.textfield}>
|
<label for={id} class={`${styles.textfield} ${isValid() ? {} : styles.invalid}`}>
|
||||||
{fieldProps.iconLeft && <span class={styles.icon}>{fieldProps.iconLeft}</span>}
|
{fieldProps.iconLeft && <span class={styles.icon}>{fieldProps.iconLeft}</span>}
|
||||||
<input id={id} type="text" {...rest} />
|
<input
|
||||||
|
id={id}
|
||||||
|
type="text"
|
||||||
|
onInput={validate}
|
||||||
|
onChange={onSubmit}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
{fieldProps.iconRight && <span class={styles.icon}>{fieldProps.iconRight}</span>}
|
{fieldProps.iconRight && <span class={styles.icon}>{fieldProps.iconRight}</span>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextField.Number = (props: TextFieldProps) => (
|
||||||
|
<TextField
|
||||||
|
inputFilter={value => /^\d*$/.test(value)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export default TextField;
|
export default TextField;
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,22 @@ import Sidebar from '../../components/Sidebar';
|
||||||
import SettingsGeneral from './launcher/SettingsGeneral';
|
import SettingsGeneral from './launcher/SettingsGeneral';
|
||||||
import SettingsAppearance from './launcher/SettingsAppearance';
|
import SettingsAppearance from './launcher/SettingsAppearance';
|
||||||
import SettingsAccounts from './game/SettingsAccounts';
|
import SettingsAccounts from './game/SettingsAccounts';
|
||||||
|
import SettingsMinecraft from './game/SettingsMinecraft';
|
||||||
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
|
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
|
||||||
import ErrorBoundary from '~ui/components/ErrorBoundary';
|
import ErrorBoundary from '~ui/components/ErrorBoundary';
|
||||||
|
|
||||||
function SettingsRoutes() {
|
function SettingsRoutes() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Launcher Settings */}
|
||||||
<Route path="/" component={SettingsGeneral} />
|
<Route path="/" component={SettingsGeneral} />
|
||||||
<Route path="/appearance" component={SettingsAppearance} />
|
<Route path="/appearance" component={SettingsAppearance} />
|
||||||
|
|
||||||
|
{/* Game Settings */}
|
||||||
|
<Route path="/minecraft" component={SettingsMinecraft} />
|
||||||
<Route path="/accounts" component={SettingsAccounts} />
|
<Route path="/accounts" component={SettingsAccounts} />
|
||||||
|
|
||||||
|
{/* About */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
desktop/src/ui/pages/settings/game/SettingsMinecraft.tsx
Normal file
54
desktop/src/ui/pages/settings/game/SettingsMinecraft.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { LayoutTopIcon, Maximize01Icon, XIcon } from '@untitled-theme/icons-solid';
|
||||||
|
import TextField from '~ui/components/base/TextField';
|
||||||
|
import Toggle from '~ui/components/base/Toggle';
|
||||||
|
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||||
|
import SettingsRow from '~ui/components/SettingsRow';
|
||||||
|
import Sidebar from '~ui/components/Sidebar';
|
||||||
|
import useSettingsContext from '~ui/hooks/useSettings';
|
||||||
|
|
||||||
|
function SettingsMinecraft() {
|
||||||
|
const settings = useSettingsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar.Page>
|
||||||
|
<h1>Global Game Settings</h1>
|
||||||
|
<ScrollableContainer>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title="Force Fullscreen"
|
||||||
|
description="Force Minecraft to start in fullscreen mode"
|
||||||
|
icon={<Maximize01Icon />}
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
defaultChecked={settings.force_fullscreen ?? false}
|
||||||
|
onChecked={value => settings.force_fullscreen = value}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title="Resolution"
|
||||||
|
description="The game window resolution"
|
||||||
|
icon={<LayoutTopIcon />}
|
||||||
|
>
|
||||||
|
<div class="grid grid-justify-center grid-items-center grid-cols-[70px_24px_70px]">
|
||||||
|
<TextField.Number
|
||||||
|
value={settings.resolution[0]}
|
||||||
|
onValidSubmit={(value) => {
|
||||||
|
settings.resolution = [Number.parseInt(value), settings.resolution[1]];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<XIcon />
|
||||||
|
<TextField.Number
|
||||||
|
onValidSubmit={(value) => {
|
||||||
|
settings.resolution = [settings.resolution[0], Number.parseInt(value)];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
</ScrollableContainer>
|
||||||
|
</Sidebar.Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsMinecraft;
|
||||||
Loading…
Add table
Reference in a new issue