) {
return (
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- {props.children}
-
-
-
-
+
+
+ {props.children}
+
+
+
);
}
export function SelectOption(props: PropsWithChildren<{ value: string; default?: boolean }>) {
return (
-
- {props.children}
-
+ {props.children}
+
-
-
+
+
);
}
diff --git a/packages/ui/src/forms/FormField.tsx b/packages/ui/src/forms/FormField.tsx
index ba3812b10..2ca44a0de 100644
--- a/packages/ui/src/forms/FormField.tsx
+++ b/packages/ui/src/forms/FormField.tsx
@@ -1,4 +1,4 @@
-import { PropsWithChildren, useId } from 'react';
+import { PropsWithChildren, ReactNode, useId } from 'react';
import { useFormContext } from 'react-hook-form';
export interface UseFormFieldProps extends PropsWithChildren {
@@ -19,9 +19,10 @@ export const useFormField = (props: P) => {
};
};
-interface FormFieldProps extends UseFormFieldProps {
+interface FormFieldProps extends Omit {
id: string;
error?: string;
+ label?: string | ReactNode;
}
export const FormField = (props: FormFieldProps) => {
diff --git a/packages/ui/src/forms/Input.tsx b/packages/ui/src/forms/Input.tsx
index b046c8843..a8ed2c053 100644
--- a/packages/ui/src/forms/Input.tsx
+++ b/packages/ui/src/forms/Input.tsx
@@ -1,5 +1,10 @@
-import { forwardRef } from 'react';
+import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
+import zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
+import zxcvbnEnPackage from '@zxcvbn-ts/language-en';
+import clsx from 'clsx';
+import { forwardRef, useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
+import { useDebounce, useDebouncedCallback } from 'use-debounce';
import * as Root from '../Input';
import { FormField, UseFormFieldProps, useFormField } from './FormField';
@@ -17,16 +22,96 @@ export const Input = forwardRef((props, ref) => {
);
});
-export const PasswordInput = forwardRef((props, ref) => {
- const { formFieldProps, childProps } = useFormField(props);
+export interface PasswordInputProps extends UseFormFieldProps, Root.InputProps {
+ name: string;
+ showStrength?: boolean;
+}
+
+const PasswordStrengthMeter = (props: { password: string }) => {
+ const [strength, setStrength] = useState<{ label: string; score: number }>();
+ const updateStrength = useDebouncedCallback(
+ () => setStrength(props.password ? getPasswordStrength(props.password) : undefined),
+ 100
+ );
+
+ // TODO: Remove duplicate in @sd/client
+ function getPasswordStrength(password: string): { label: string; score: number } {
+ const ratings = ['Poor', 'Weak', 'Good', 'Strong', 'Perfect'];
+
+ zxcvbnOptions.setOptions({
+ dictionary: {
+ ...zxcvbnCommonPackage.dictionary,
+ ...zxcvbnEnPackage.dictionary
+ },
+ graphs: zxcvbnCommonPackage.adjacencyGraphs,
+ translations: zxcvbnEnPackage.translations
+ });
+
+ const result = zxcvbn(password);
+ return { label: ratings[result.score]!, score: result.score };
+ }
+
+ useEffect(() => updateStrength(), [props.password]);
return (
-
-
-
+
+ {strength && (
+
+ {strength.label}
+
+ )}
+
+
+ {strength && (
+
+ )}
+
+
);
-});
+};
+
+export const PasswordInput = forwardRef(
+ ({ showStrength, ...props }, ref) => {
+ const { formFieldProps, childProps } = useFormField(props);
+ const { watch } = useFormContext();
+
+ return (
+
+ {formFieldProps.label}
+ {showStrength && }
+ >
+ }
+ >
+
+
+ );
+ }
+);
diff --git a/packages/ui/src/forms/Select.tsx b/packages/ui/src/forms/Select.tsx
new file mode 100644
index 000000000..96f167d60
--- /dev/null
+++ b/packages/ui/src/forms/Select.tsx
@@ -0,0 +1,24 @@
+import { FieldValues, UseControllerProps, useController } from 'react-hook-form';
+import * as Root from '../Select';
+import { FormField, UseFormFieldProps, useFormField } from './FormField';
+
+export interface SelectProps
+ extends Omit,
+ Omit,
+ UseControllerProps {}
+
+export const Select = (props: SelectProps) => {
+ const { formFieldProps, childProps } = useFormField(props);
+ const { field } = useController({ name: props.name });
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/ui/src/forms/index.ts b/packages/ui/src/forms/index.ts
index 35f5f502e..c60e36a1a 100644
--- a/packages/ui/src/forms/index.ts
+++ b/packages/ui/src/forms/index.ts
@@ -3,4 +3,5 @@ export * from './FormField';
export * from './CheckBox';
export * from './Input';
export * from './Switch';
+export * from './Select';
export * as RadioGroup from './RadioGroup';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cad70ee098b644d2174406ff6bfeccbcf58f0eaa..ed8de25439c9e1f4e0cab838f9e487e9b89a2fc4 100644
GIT binary patch
delta 7261
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
delta 6685
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
TJv4${s5S4~$+jm2S62ToTR@bw