Merge pull request #266 from Hunter275/issue-261-password-generator
PSK Generator
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"crypto-random-string": "^5.0.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
||||||
|
|||||||
Generated
+17
@@ -80,6 +80,9 @@ importers:
|
|||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
crypto-random-string:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
immer:
|
immer:
|
||||||
specifier: ^10.1.1
|
specifier: ^10.1.1
|
||||||
version: 10.1.1
|
version: 10.1.1
|
||||||
@@ -1901,6 +1904,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
crypto-random-string@5.0.0:
|
||||||
|
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
cssesc@3.0.0:
|
cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2929,6 +2936,10 @@ packages:
|
|||||||
turf-jsts@1.2.3:
|
turf-jsts@1.2.3:
|
||||||
resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==}
|
resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==}
|
||||||
|
|
||||||
|
type-fest@2.19.0:
|
||||||
|
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
typescript@5.5.2:
|
typescript@5.5.2:
|
||||||
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
|
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -5293,6 +5304,10 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
crypto-random-string@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
type-fest: 2.19.0
|
||||||
|
|
||||||
cssesc@3.0.0: {}
|
cssesc@3.0.0: {}
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
@@ -6365,6 +6380,8 @@ snapshots:
|
|||||||
|
|
||||||
turf-jsts@1.2.3: {}
|
turf-jsts@1.2.3: {}
|
||||||
|
|
||||||
|
type-fest@2.19.0: {}
|
||||||
|
|
||||||
typescript@5.5.2: {}
|
typescript@5.5.2: {}
|
||||||
|
|
||||||
typewise-core@1.2.0: {}
|
typewise-core@1.2.0: {}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface BaseFormBuilderProps<T> {
|
|||||||
disabledBy?: DisabledBy<T>[];
|
disabledBy?: DisabledBy<T>[];
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
validationText?: string;
|
||||||
properties?: Record<string, unknown>;
|
properties?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,11 +40,12 @@ export interface DynamicFormProps<T extends FieldValues> {
|
|||||||
onSubmit: SubmitHandler<T>;
|
onSubmit: SubmitHandler<T>;
|
||||||
submitType?: "onChange" | "onSubmit";
|
submitType?: "onChange" | "onSubmit";
|
||||||
hasSubmitButton?: boolean;
|
hasSubmitButton?: boolean;
|
||||||
// defaultValues?: DeepPartial<T>;
|
|
||||||
defaultValues?: DefaultValues<T>;
|
defaultValues?: DefaultValues<T>;
|
||||||
fieldGroups: {
|
fieldGroups: {
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
valid?: boolean;
|
||||||
|
validationText?: string;
|
||||||
fields: FieldProps<T>[];
|
fields: FieldProps<T>[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -98,6 +100,11 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
key={field.label}
|
key={field.label}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
description={field.description}
|
description={field.description}
|
||||||
|
valid={
|
||||||
|
field.validationText === undefined ||
|
||||||
|
field.validationText === ""
|
||||||
|
}
|
||||||
|
validationText={field.validationText}
|
||||||
>
|
>
|
||||||
<DynamicFormField
|
<DynamicFormField
|
||||||
field={field}
|
field={field}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import {
|
|||||||
GenericInput,
|
GenericInput,
|
||||||
type InputFieldProps,
|
type InputFieldProps,
|
||||||
} from "@components/Form/FormInput.js";
|
} from "@components/Form/FormInput.js";
|
||||||
|
import {
|
||||||
|
PasswordGenerator,
|
||||||
|
type PasswordGeneratorProps,
|
||||||
|
} from "@components/Form/FormPasswordGenerator.js";
|
||||||
import {
|
import {
|
||||||
type SelectFieldProps,
|
type SelectFieldProps,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
@@ -15,7 +19,8 @@ import type { Control, FieldValues } from "react-hook-form";
|
|||||||
export type FieldProps<T> =
|
export type FieldProps<T> =
|
||||||
| InputFieldProps<T>
|
| InputFieldProps<T>
|
||||||
| SelectFieldProps<T>
|
| SelectFieldProps<T>
|
||||||
| ToggleFieldProps<T>;
|
| ToggleFieldProps<T>
|
||||||
|
| PasswordGeneratorProps<T>;
|
||||||
|
|
||||||
export interface DynamicFormFieldProps<T extends FieldValues> {
|
export interface DynamicFormFieldProps<T extends FieldValues> {
|
||||||
field: FieldProps<T>;
|
field: FieldProps<T>;
|
||||||
@@ -44,6 +49,14 @@ export function DynamicFormField<T extends FieldValues>({
|
|||||||
return (
|
return (
|
||||||
<SelectInput field={field} control={control} disabled={disabled} />
|
<SelectInput field={field} control={control} disabled={disabled} />
|
||||||
);
|
);
|
||||||
|
case "passwordGenerator":
|
||||||
|
return (
|
||||||
|
<PasswordGenerator
|
||||||
|
field={field}
|
||||||
|
control={control}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "multiSelect":
|
case "multiSelect":
|
||||||
return <div>tmp</div>;
|
return <div>tmp</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type {
|
||||||
|
BaseFormBuilderProps,
|
||||||
|
GenericFormElementProps,
|
||||||
|
} from "@components/Form/DynamicForm.js";
|
||||||
|
import { Generator } from "@components/UI/Generator.js";
|
||||||
|
import type { ChangeEventHandler, MouseEventHandler } from "react";
|
||||||
|
import { Controller, type FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||||
|
type: "passwordGenerator";
|
||||||
|
devicePSKBitCount: number;
|
||||||
|
inputChange: ChangeEventHandler;
|
||||||
|
selectChange: (event: string) => void;
|
||||||
|
buttonClick: MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordGenerator<T extends FieldValues>({
|
||||||
|
control,
|
||||||
|
field,
|
||||||
|
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={field.name}
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Generator
|
||||||
|
devicePSKBitCount={field.devicePSKBitCount}
|
||||||
|
inputChange={field.inputChange}
|
||||||
|
selectChange={field.selectChange}
|
||||||
|
buttonClick={field.buttonClick}
|
||||||
|
value={value}
|
||||||
|
variant={field.validationText ? "invalid" : "default"}
|
||||||
|
buttonText="Generate"
|
||||||
|
{...field.properties}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,12 +5,16 @@ export interface FieldWrapperProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
valid?: boolean;
|
||||||
|
validationText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FieldWrapper = ({
|
export const FieldWrapper = ({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
|
valid,
|
||||||
|
validationText,
|
||||||
}: FieldWrapperProps): JSX.Element => (
|
}: FieldWrapperProps): JSX.Element => (
|
||||||
<div className="pt-6 sm:pt-5">
|
<div className="pt-6 sm:pt-5">
|
||||||
<div role="group" aria-labelledby="label-notifications">
|
<div role="group" aria-labelledby="label-notifications">
|
||||||
@@ -19,6 +23,9 @@ export const FieldWrapper = ({
|
|||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<div className="max-w-lg">
|
<div className="max-w-lg">
|
||||||
<p className="text-sm text-gray-500">{description}</p>
|
<p className="text-sm text-gray-500">{description}</p>
|
||||||
|
<p hidden={valid ?? true} className="text-sm text-red-500">
|
||||||
|
{validationText}
|
||||||
|
</p>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center">{children}</div>
|
<div className="flex items-center">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useToast } from "@core/hooks/useToast.js";
|
|||||||
import { useDevice } from "@core/stores/deviceStore.js";
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
import { Protobuf } from "@meshtastic/js";
|
import { Protobuf } from "@meshtastic/js";
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
|
import cryptoRandomString from "crypto-random-string";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface SettingsPanelProps {
|
export interface SettingsPanelProps {
|
||||||
channel: Protobuf.Channel.Channel;
|
channel: Protobuf.Channel.Channel;
|
||||||
@@ -13,12 +15,20 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
const { config, connection, addChannel } = useDevice();
|
const { config, connection, addChannel } = useDevice();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [pass, setPass] = useState<string>(
|
||||||
|
fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
||||||
|
);
|
||||||
|
const [bitCount, setBits] = useState<number>(
|
||||||
|
channel?.settings?.psk.length ?? 16,
|
||||||
|
);
|
||||||
|
const [validationText, setValidationText] = useState<string>();
|
||||||
|
|
||||||
const onSubmit = (data: ChannelValidation) => {
|
const onSubmit = (data: ChannelValidation) => {
|
||||||
const channel = new Protobuf.Channel.Channel({
|
const channel = new Protobuf.Channel.Channel({
|
||||||
...data,
|
...data,
|
||||||
settings: {
|
settings: {
|
||||||
...data.settings,
|
...data.settings,
|
||||||
psk: toByteArray(data.settings.psk ?? ""),
|
psk: toByteArray(pass),
|
||||||
moduleSettings: {
|
moduleSettings: {
|
||||||
positionPrecision: data.settings.positionEnabled
|
positionPrecision: data.settings.positionEnabled
|
||||||
? data.settings.preciseLocation
|
? data.settings.preciseLocation
|
||||||
@@ -36,6 +46,38 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clickEvent = () => {
|
||||||
|
setPass(
|
||||||
|
btoa(
|
||||||
|
cryptoRandomString({
|
||||||
|
length: bitCount ?? 0,
|
||||||
|
type: "alphanumeric",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setValidationText(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePass = (input: string, count: number) => {
|
||||||
|
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
||||||
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
|
} else {
|
||||||
|
setValidationText(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const psk = e.currentTarget?.value;
|
||||||
|
setPass(psk);
|
||||||
|
validatePass(psk, bitCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectChangeEvent = (e: string) => {
|
||||||
|
const count = Number.parseInt(e);
|
||||||
|
setBits(count);
|
||||||
|
validatePass(pass, count);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DynamicForm<ChannelValidation>
|
<DynamicForm<ChannelValidation>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@@ -46,7 +88,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
...{
|
...{
|
||||||
settings: {
|
settings: {
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: pass,
|
||||||
positionEnabled:
|
positionEnabled:
|
||||||
channel?.settings?.moduleSettings?.positionPrecision !==
|
channel?.settings?.moduleSettings?.positionPrecision !==
|
||||||
undefined &&
|
undefined &&
|
||||||
@@ -76,12 +118,17 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "password",
|
type: "passwordGenerator",
|
||||||
name: "settings.psk",
|
name: "settings.psk",
|
||||||
label: "pre-Shared Key",
|
label: "pre-Shared Key",
|
||||||
description: "16, or 32 bytes",
|
description: "256, 128, or 8 bit PSKs allowed",
|
||||||
|
validationText: validationText,
|
||||||
|
devicePSKBitCount: bitCount ?? 0,
|
||||||
|
inputChange: inputChangeEvent,
|
||||||
|
selectChange: selectChangeEvent,
|
||||||
|
buttonClick: clickEvent,
|
||||||
properties: {
|
properties: {
|
||||||
// act
|
value: pass,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const buttonVariants = cva(
|
|||||||
"bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
|
"bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
|
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
|
||||||
|
success:
|
||||||
|
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||||
outline:
|
outline:
|
||||||
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
|
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
|
||||||
subtle:
|
subtle:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Button } from "@components/UI/Button.js";
|
||||||
|
import { Input } from "@components/UI/Input.js";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@components/UI/Select.js";
|
||||||
|
|
||||||
|
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||||
|
devicePSKBitCount?: number;
|
||||||
|
value: string;
|
||||||
|
variant: "default" | "invalid";
|
||||||
|
buttonText?: string;
|
||||||
|
selectChange: (event: string) => void;
|
||||||
|
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
devicePSKBitCount,
|
||||||
|
variant,
|
||||||
|
value,
|
||||||
|
buttonText,
|
||||||
|
selectChange,
|
||||||
|
inputChange,
|
||||||
|
buttonClick,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="pskInput"
|
||||||
|
variant={variant}
|
||||||
|
value={value}
|
||||||
|
onChange={inputChange}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={devicePSKBitCount?.toString()}
|
||||||
|
onValueChange={(e) => selectChange(e)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem key="bit256" value="32">
|
||||||
|
256 bit
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="bit128" value="16">
|
||||||
|
128 bit
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="bit8" value="1">
|
||||||
|
8 bit
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="success"
|
||||||
|
onClick={buttonClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Generator.displayName = "Button";
|
||||||
|
|
||||||
|
export { Generator };
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@core/utils/cn.js";
|
import { cn } from "@core/utils/cn.js";
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
const inputVariants = cva(
|
||||||
|
"flex h-10 w-full rounded-md border bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border-slate-300 dark:border-slate-700",
|
||||||
|
invalid: "border-red-500 dark:border-red-500",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
VariantProps<typeof inputVariants> {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
action?: {
|
action?: {
|
||||||
@@ -14,7 +31,7 @@ export interface InputProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, prefix, suffix, action, ...props }, ref) => {
|
({ className, variant, prefix, suffix, action, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
@@ -24,9 +41,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
|
||||||
action && "pr-8",
|
action && "pr-8",
|
||||||
className,
|
className,
|
||||||
|
inputVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -51,4 +68,4 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
);
|
);
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input };
|
export { Input, inputVariants };
|
||||||
|
|||||||
Reference in New Issue
Block a user