feat: move to secure OS credential storage so that you never get logged out again
This commit is contained in:
parent
d6e0a8350a
commit
ef08fa6114
15 changed files with 493 additions and 276 deletions
49
src/mainview/components/options/OptionInput.tsx
Normal file
49
src/mainview/components/options/OptionInput.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import classNames from "classnames";
|
||||
import { ChangeEventHandler, FocusEventHandler, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useOptionContext } from "./OptionSpace";
|
||||
|
||||
export function OptionInput (data: {
|
||||
name: string;
|
||||
type: HTMLInputTypeAttribute;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
})
|
||||
{
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const option = useOptionContext({
|
||||
onOptionEnterPress ()
|
||||
{
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent">
|
||||
<span className={twMerge("text-base-content/80", classNames({
|
||||
"text-primary-content": option.focused
|
||||
}))}>{data.icon}</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={data.name}
|
||||
name={data.name}
|
||||
value={data.value}
|
||||
defaultValue={data.defaultValue}
|
||||
type={data.type}
|
||||
onFocus={() => option.focus()}
|
||||
placeholder={data.placeholder}
|
||||
onChange={data.onChange}
|
||||
onBlur={data.onBlur}
|
||||
className={twMerge(
|
||||
"input grow rounded-full ring-primary-content focus:ring-3",
|
||||
data.className,
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
89
src/mainview/components/options/OptionSpace.tsx
Normal file
89
src/mainview/components/options/OptionSpace.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { FocusContext, FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { createContext, JSX, useContext, useEffect, useMemo } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const OptionContext = createContext(
|
||||
{} as {
|
||||
focused: boolean;
|
||||
focus: (focusDetails?: FocusDetails | undefined) => void;
|
||||
eventTarget: EventTarget;
|
||||
},
|
||||
);
|
||||
|
||||
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
||||
{
|
||||
const context = useContext(OptionContext);
|
||||
useEffect(() =>
|
||||
{
|
||||
if (params?.onOptionEnterPress)
|
||||
{
|
||||
context.eventTarget.addEventListener(
|
||||
"onEnterPress",
|
||||
params.onOptionEnterPress,
|
||||
);
|
||||
}
|
||||
|
||||
return () =>
|
||||
{
|
||||
if (params?.onOptionEnterPress)
|
||||
{
|
||||
context.eventTarget.removeEventListener(
|
||||
"onEnterPress",
|
||||
params.onOptionEnterPress,
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [context.eventTarget]);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function OptionSpace (data: {
|
||||
id?: string;
|
||||
className?: string;
|
||||
focusable?: boolean;
|
||||
children: JSX.Element;
|
||||
label?: string | JSX.Element;
|
||||
})
|
||||
{
|
||||
const eventTarget = useMemo(() => new EventTarget(), []);
|
||||
const { ref, focused, focusSelf, focusKey, hasFocusedChild } = useFocusable({
|
||||
focusKey: data.id,
|
||||
focusable: data.focusable !== false,
|
||||
trackChildren: true,
|
||||
onEnterPress ()
|
||||
{
|
||||
eventTarget.dispatchEvent(new CustomEvent("onEnterPress"));
|
||||
},
|
||||
});
|
||||
|
||||
return (<FocusContext value={focusKey}>
|
||||
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
||||
<li
|
||||
ref={ref}
|
||||
className={twMerge("flex sm:p-2 md:p-4 pl-8! rounded-full bg-base-content/1", classNames(
|
||||
{
|
||||
"text-primary-content bg-primary ": focused || hasFocusedChild,
|
||||
}),
|
||||
data.className,
|
||||
)}
|
||||
>
|
||||
<div className="label flex-1 md:text-lg pr-4">
|
||||
{typeof data.label === "string" ? (
|
||||
<label
|
||||
className={classNames({
|
||||
"text-primary-content font-semibold": focused,
|
||||
})}
|
||||
>
|
||||
{data.label}
|
||||
</label>
|
||||
) : (
|
||||
data.label
|
||||
)}
|
||||
</div>
|
||||
{data.children}
|
||||
</li>
|
||||
</OptionContext>
|
||||
</FocusContext>
|
||||
);
|
||||
}
|
||||
38
src/mainview/components/options/SettingsAppForm.tsx
Normal file
38
src/mainview/components/options/SettingsAppForm.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||
import { HTMLInputTypeAttribute, JSX } from "react";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import classNames from "classnames";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
|
||||
// export useFieldContext for use in your custom components
|
||||
export const { fieldContext, formContext, useFieldContext } =
|
||||
createFormHookContexts();
|
||||
|
||||
export const { useAppForm: useSettingsForm, useTypedAppFormContext: useSettingsFormContext } = createFormHook({
|
||||
fieldContext,
|
||||
formContext,
|
||||
fieldComponents: { FormOption },
|
||||
formComponents: {}
|
||||
});
|
||||
|
||||
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
|
||||
{
|
||||
const field = useFieldContext<string>();
|
||||
return <OptionSpace label={<div className="flex gap-2">
|
||||
{data.label}
|
||||
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
|
||||
{field.state.meta.errors.map(e => e.message).join(',')}
|
||||
</div>}
|
||||
</div>}>
|
||||
<OptionInput
|
||||
icon={data.icon}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
type={data.type}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
placeholder={data.placeholder}
|
||||
className={classNames({ "ring-4 ring-accent": field.getMeta().isDirty })}
|
||||
/>
|
||||
</OptionSpace>;;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue