- Max. 32 characters
+ <>
+
+
+
+ Enter the nickname of the service account you wish to add to the
+ team {props.teamName}
+
+
+
{
+ updateFormFieldState({
+ field: "nickname",
+ value: e.target.value,
+ });
+ }}
+ placeholder={"ExampleName"}
+ maxLength={32}
+ required
+ {...nicknameFieldProps}
+ />
+
+ Max. 32 characters
+
+
+ {error &&
{error}}
- {error && {error}}
-
-
- )}
- {serviceAccountAdded ? null : (
-
-
- Add Service Account
-
-
+
+
+
+ Add Service Account
+
+
+ >
)}
);
diff --git a/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts b/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts
index 39417d4a6..be898eb33 100644
--- a/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts
+++ b/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import {
ParseError,
RequestBodyParseError,
@@ -57,20 +57,105 @@ export function useStrongForm<
const [submitOutput, setSubmitOutput] = useState
();
const [submitError, setSubmitError] = useState();
const [inputErrors, setInputErrors] = useState();
+ const [fieldInteractions, setFieldInteractions] = useState<
+ Partial<
+ Record<
+ keyof Inputs,
+ {
+ hasFocused: boolean;
+ hasBlurred: boolean;
+ }
+ >
+ >
+ >({});
+ const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
+
+ const isValueEmpty = (value: unknown) => {
+ if (typeof value === "string") {
+ return value.trim() === "";
+ }
+ return value === undefined || value === null;
+ };
const isReady = useMemo(() => {
if (!props.validators) return true;
for (const key in props.validators) {
const validator = props.validators[key];
- const value = props.inputs[key];
+ const value = props.inputs[key as keyof Inputs];
// NOTE: Expand the checks as more validators are added
- if (validator?.required) {
- if (typeof value === "string" && value.trim() === "") return false;
- if (value === undefined || value === null) return false;
+ if (validator?.required && isValueEmpty(value)) {
+ return false;
}
}
return true;
- }, [props.inputs]);
+ }, [props.inputs, props.validators]);
+
+ const getFieldState = useCallback(
+ (field: K) => {
+ const validator = props.validators?.[field];
+ const value = props.inputs[field];
+ const isRequired = Boolean(validator?.required);
+ const rawInvalid = isRequired && isValueEmpty(value);
+ const interactions = fieldInteractions[field];
+ const hasFinishedInteraction =
+ Boolean(interactions?.hasFocused && interactions?.hasBlurred) ||
+ hasAttemptedSubmit;
+ const isInvalid = rawInvalid && hasFinishedInteraction;
+ return {
+ isRequired,
+ isInvalid,
+ };
+ },
+ [fieldInteractions, hasAttemptedSubmit, props.inputs, props.validators]
+ );
+
+ const markFieldInteraction = useCallback(
+ (field: keyof Inputs, type: "focus" | "blur") => {
+ setFieldInteractions((prev) => {
+ const current = prev[field] ?? { hasFocused: false, hasBlurred: false };
+ const next =
+ type === "focus"
+ ? { ...current, hasFocused: true }
+ : { ...current, hasBlurred: true };
+ if (
+ current.hasFocused === next.hasFocused &&
+ current.hasBlurred === next.hasBlurred
+ ) {
+ return prev;
+ }
+ return { ...prev, [field]: next };
+ });
+ },
+ []
+ );
+
+ const getFieldInteractionProps = useCallback(
+ (field: keyof Inputs) => ({
+ onFocus: () => markFieldInteraction(field, "focus"),
+ onBlur: () => markFieldInteraction(field, "blur"),
+ }),
+ [markFieldInteraction]
+ );
+
+ const getFieldComponentProps = useCallback(
+ (field: keyof Inputs, options?: { disabled?: boolean }) => {
+ const fieldState = getFieldState(field);
+ const modifiers: ("invalid" | "disabled")[] = [];
+ if (fieldState.isInvalid) {
+ modifiers.push("invalid" as const);
+ }
+ if (options?.disabled) {
+ modifiers.push("disabled" as const);
+ }
+ const interactionProps = getFieldInteractionProps(field);
+ return {
+ ...interactionProps,
+ "aria-invalid": fieldState.isInvalid,
+ csModifiers: modifiers,
+ };
+ },
+ [getFieldInteractionProps, getFieldState]
+ );
useEffect(() => {
if (refining || submitting) {
@@ -106,6 +191,7 @@ export function useStrongForm<
}, [props.inputs]);
const submit = async () => {
+ setHasAttemptedSubmit(true);
if (submitting) {
const error = new Error("Form is already submitting!");
if (props.onSubmitError) {
@@ -193,5 +279,8 @@ export function useStrongForm<
refineError,
inputErrors,
isReady,
+ getFieldState,
+ getFieldInteractionProps,
+ getFieldComponentProps,
};
}
diff --git a/packages/cyberstorm-theme/src/components/Button/Button.css b/packages/cyberstorm-theme/src/components/Button/Button.css
index d9ae95aca..75ca95960 100644
--- a/packages/cyberstorm-theme/src/components/Button/Button.css
+++ b/packages/cyberstorm-theme/src/components/Button/Button.css
@@ -55,14 +55,14 @@
--button-text-color: var(--button-primary-text-color--default);
--button-icon-color: var(--button-primary-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-primary-bg-color--hover);
--button-text-color: var(--button-primary-text-color--hover);
--button-icon-color: var(--button-primary-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-primary-bg-color--active);
--button-text-color: var(--button-primary-text-color--active);
--button-icon-color: var(--button-primary-icon-color--active);
@@ -74,14 +74,14 @@
--button-text-color: var(--button-secondary-text-color--default);
--button-icon-color: var(--button-secondary-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-secondary-bg-color--hover);
--button-text-color: var(--button-secondary-text-color--hover);
--button-icon-color: var(--button-secondary-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-secondary-bg-color--active);
--button-text-color: var(--button-secondary-text-color--active);
--button-icon-color: var(--button-secondary-icon-color--active);
@@ -93,14 +93,14 @@
--button-text-color: var(--button-accent-text-color--default);
--button-icon-color: var(--button-accent-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-accent-bg-color--hover);
--button-text-color: var(--button-accent-text-color--hover);
--button-icon-color: var(--button-accent-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-accent-bg-color--active);
--button-text-color: var(--button-accent-text-color--active);
--button-icon-color: var(--button-accent-icon-color--active);
@@ -115,7 +115,7 @@
background: var(--button-special-background--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-text-color: var(--button-special-text-color--hover);
--button-icon-color: var(--button-special-icon-color--hover);
@@ -125,7 +125,7 @@
background: var(--button-special-background--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-text-color: var(--button-special-text-color--active);
--button-icon-color: var(--button-special-icon-color--active);
--button-border: var(--button-special-border--active);
@@ -140,14 +140,14 @@
--button-text-color: var(--button-success-text-color--default);
--button-icon-color: var(--button-success-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-success-bg-color--hover);
--button-text-color: var(--button-success-text-color--hover);
--button-icon-color: var(--button-success-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-success-bg-color--active);
--button-text-color: var(--button-success-text-color--active);
--button-icon-color: var(--button-success-icon-color--active);
@@ -159,14 +159,14 @@
--button-text-color: var(--button-info-text-color--default);
--button-icon-color: var(--button-info-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-info-bg-color--hover);
--button-text-color: var(--button-info-text-color--hover);
--button-icon-color: var(--button-info-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-info-bg-color--active);
--button-text-color: var(--button-info-text-color--active);
--button-icon-color: var(--button-info-icon-color--active);
@@ -178,14 +178,14 @@
--button-text-color: var(--button-warning-text-color--default);
--button-icon-color: var(--button-warning-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-warning-bg-color--hover);
--button-text-color: var(--button-warning-text-color--hover);
--button-icon-color: var(--button-warning-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-warning-bg-color--active);
--button-text-color: var(--button-warning-text-color--active);
--button-icon-color: var(--button-warning-icon-color--active);
@@ -197,14 +197,14 @@
--button-text-color: var(--button-danger-text-color--default);
--button-icon-color: var(--button-danger-icon-color--default);
- &:where(:hover),
+ &:where(:hover:not(:disabled)),
&[data-state="open"] {
--button-background-color: var(--button-danger-bg-color--hover);
--button-text-color: var(--button-danger-text-color--hover);
--button-icon-color: var(--button-danger-icon-color--hover);
}
- &:where(:active) {
+ &:where(:active:not(:disabled)) {
--button-background-color: var(--button-danger-bg-color--active);
--button-text-color: var(--button-danger-text-color--active);
--button-icon-color: var(--button-danger-icon-color--active);
@@ -226,7 +226,6 @@
.button[disabled] {
opacity: 0.5;
- pointer-events: none;
}
.button:where(.button--only-icon) {
diff --git a/packages/cyberstorm-theme/src/components/TextInput/TextInput.css b/packages/cyberstorm-theme/src/components/TextInput/TextInput.css
index 95cff966b..818b73f41 100644
--- a/packages/cyberstorm-theme/src/components/TextInput/TextInput.css
+++ b/packages/cyberstorm-theme/src/components/TextInput/TextInput.css
@@ -27,6 +27,10 @@
&:focus-within .text-input__left-icon {
--text-input-left-icon-color: var(--input-icon-color--focus);
}
+
+ &.text-input__wrapper--invalid .text-input__left-icon {
+ --text-input-left-icon-color: var(--input-icon-color--invalid);
+ }
}
.text-input[value] {
@@ -55,12 +59,24 @@
--right-padding-bonus: var(--space-16);
}
- .text-input:hover {
+ .text-input:where(.text-input--invalid),
+ .text-input:where(.text-input--invalid):hover,
+ .text-input:where(.text-input--invalid):focus-within {
+ --text-input-text-color: var(--input-text-color--invalid);
+ --text-input-background-color: var(--input-bg-color--invalid);
+ --text-input-border-color: var(--input-border-color--invalid);
+ }
+
+ .text-input:disabled {
+ cursor: not-allowed;
+ }
+
+ .text-input:hover:not(:disabled) {
--text-input-background-color: var(--input-bg-color--hover);
--text-input-border-color: var(--input-border-color--hover);
}
- .text-input:focus-within {
+ .text-input:focus-within:not(:disabled) {
--text-input-text-color: var(--input-text-color--focus);
--text-input-background-color: var(--input-bg-color--focus);
--text-input-border-color: var(--input-border-color--focus);
diff --git a/packages/cyberstorm-theme/src/components/componentsColors.css b/packages/cyberstorm-theme/src/components/componentsColors.css
index 7f0604a23..7839e52ab 100644
--- a/packages/cyberstorm-theme/src/components/componentsColors.css
+++ b/packages/cyberstorm-theme/src/components/componentsColors.css
@@ -263,16 +263,20 @@
--input-bg-color--default: var(--color-nightsky-a4);
--input-bg-color--focus: var(--color-nightsky-1);
--input-bg-color--hover: var(--color-nightsky-a6);
+ --input-bg-color--invalid: var(--color-accent-red-2);
--input-border-color--default: var(--color-nightsky-a10);
--input-border-color--focus: var(--color-cyber-green-7);
--input-border-color--hover: var(--color-nightsky-a10);
+ --input-border-color--invalid: var(--color-accent-red-7);
--input-icon-color--default: var(--color-text-tertiary);
--input-icon-color--focus: var(--color-text-secondary);
--input-icon-color--hover: var(--color-text-tertiary);
+ --input-icon-color--invalid: var(--color-accent-red-8);
--input-placeholder-color: var(--color-text-tertiary);
--input-text-color--default: var(--color-text-secondary);
--input-text-color--focus: var(--color-text-primary);
--input-text-color--hover: var(--color-text-secondary);
+ --input-text-color--invalid: var(--color-text-primary);
--kbd-bg-color--default: hsl(0deg 0 0% / 0);
--kbd-border-color--default: var(--color-nightsky-a10);
--kbd-text-color--default: var(--color-text-tertiary);