diff --git a/src/app/content/ui/editable-error.tsx b/src/app/content/ui/editable-error.tsx
new file mode 100644
index 00000000..bc6e9095
--- /dev/null
+++ b/src/app/content/ui/editable-error.tsx
@@ -0,0 +1,24 @@
+import {
+ Editable,
+ EditableError,
+ EditableInput,
+ EditablePreview,
+} from "@/components/ui/editable";
+
+export default function EditableErrorDemo() {
+ const error = "This field is required";
+
+ return (
+
+
+
+
+ {error}
+
+
+ );
+}
diff --git a/src/app/demo/[name]/ui/editable.tsx b/src/app/demo/[name]/ui/editable.tsx
index 0bf12ccc..e2316275 100644
--- a/src/app/demo/[name]/ui/editable.tsx
+++ b/src/app/demo/[name]/ui/editable.tsx
@@ -23,5 +23,6 @@ export const editable = {
},
components: {
Textarea: { component: "editable-textarea" },
+ "Editable with error": { component: "editable-error" },
},
};
diff --git a/src/components/ui/editable.tsx b/src/components/ui/editable.tsx
index 25d237b3..db18d5e8 100644
--- a/src/components/ui/editable.tsx
+++ b/src/components/ui/editable.tsx
@@ -57,6 +57,8 @@ interface UseEditableProps {
selectAllOnFocus?: boolean;
/** Activation mode: 'click' or 'dblclick' */
activationMode?: ActivationMode;
+ /** Error message to display */
+ hasError?: boolean;
/** Callback when value is submitted */
onSubmit?: (value: string) => void;
/** Callback when value changes */
@@ -85,6 +87,7 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
startWithEditView = false,
selectAllOnFocus = true,
activationMode = "click",
+ hasError = false,
onSubmit,
onChange,
onValueChange,
@@ -92,7 +95,9 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
onEdit,
} = props;
- const [isEditing, setIsEditing] = React.useState(startWithEditView);
+ const [isEditing, setIsEditing] = React.useState(
+ startWithEditView || hasError,
+ );
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [previousValue, setPreviousValue] = React.useState(defaultValue);
const inputRef = React.useRef(
@@ -114,14 +119,14 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
if (!isControlled) {
setInternalValue(previousValue);
}
- setIsEditing(false);
+ setIsEditing(hasError || false);
onCancel?.(previousValue);
- }, [isControlled, previousValue, onCancel]);
+ }, [isControlled, previousValue, onCancel, hasError]);
const submitEdit = React.useCallback(() => {
- setIsEditing(false);
+ setIsEditing(hasError || false);
onSubmit?.(value);
- }, [value, onSubmit]);
+ }, [value, onSubmit, hasError]);
const handleChange = React.useCallback(
(newValue: string) => {
@@ -169,7 +174,7 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
// Editable Root Component
-const editableVariants = cva("inline-flex flex-col gap-1", {
+const editableVariants = cva("inline-flex flex-col gap-1 relative", {
variants: {
size: {
sm: "text-sm",
@@ -203,6 +208,8 @@ interface EditableProps
selectAllOnFocus?: boolean;
/** Activation mode: 'click' or 'dblclick' */
activationMode?: ActivationMode;
+ /** Error message to display */
+ hasError?: boolean;
/** Callback when value is submitted */
onSubmit?: (value: string) => void;
/** Callback when value changes */
@@ -227,6 +234,7 @@ function Editable({
startWithEditView = false,
selectAllOnFocus = true,
activationMode = "click",
+ hasError = false,
onSubmit,
onChange,
onValueChange,
@@ -235,7 +243,9 @@ function Editable({
children,
...props
}: EditableProps) {
- const [isEditing, setIsEditing] = React.useState(startWithEditView);
+ const [isEditing, setIsEditing] = React.useState(
+ startWithEditView || hasError,
+ );
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [previousValue, setPreviousValue] = React.useState(defaultValue);
const inputRef = React.useRef(
@@ -257,14 +267,14 @@ function Editable({
if (!isControlled) {
setInternalValue(previousValue);
}
- setIsEditing(false);
+ setIsEditing(hasError || false);
onCancel?.(previousValue);
- }, [isControlled, previousValue, onCancel]);
+ }, [isControlled, previousValue, onCancel, hasError]);
const submitEdit = React.useCallback(() => {
- setIsEditing(false);
+ setIsEditing(hasError || false);
onSubmit?.(value);
- }, [value, onSubmit]);
+ }, [value, onSubmit, hasError]);
const handleChange = React.useCallback(
(newValue: string) => {
@@ -355,7 +365,7 @@ const editablePreviewVariants = cva(
[
"cursor-text rounded-md px-2 py-1 transition-colors",
"hover:bg-neutral-bg",
- "min-h-[2rem] flex items-center whitespace-pre-line break-words",
+ "min-h-8 flex items-center whitespace-pre-line break-words",
].join(" "),
{
variants: {
@@ -474,7 +484,7 @@ function EditableInput({ className, ...props }: EditableInputProps) {
}
}}
className={cn(
- "w-full border-2 bg-transparent dark:bg-transparent focus-visible:ring-0 focus-visible:border-primary transition-colors",
+ "w-full border-2 bg-transparent h-8 dark:bg-transparent focus-visible:ring-0 focus-visible:border-primary transition-colors",
className,
)}
{...props}
@@ -649,6 +659,47 @@ function EditableSubmitTrigger({
);
}
+interface EditableErrorProps extends React.HTMLAttributes {
+ errors?: { message?: string }[];
+}
+
+function EditableError({
+ errors,
+ children,
+ className,
+ ...props
+}: EditableErrorProps) {
+ const errorMessages = errors?.filter(Boolean) || [];
+
+ if (errorMessages.length === 0 && !children) {
+ return null;
+ }
+
+ return (
+
+ {children ||
+ (errorMessages.length === 1 ? (
+
{errorMessages[0]?.message}
+ ) : (
+
+ {errorMessages.map((error, index) => (
+ - {error?.message}
+ ))}
+
+ ))}
+
+ );
+}
+
export {
Editable,
EditableRootProvider,
@@ -659,6 +710,7 @@ export {
EditableEditTrigger,
EditableCancelTrigger,
EditableSubmitTrigger,
+ EditableError,
editableVariants,
editablePreviewVariants,
useEditable,
diff --git a/src/lib/docsite/docsite-registry.ts b/src/lib/docsite/docsite-registry.ts
index 6252ed14..8e2af2db 100644
--- a/src/lib/docsite/docsite-registry.ts
+++ b/src/lib/docsite/docsite-registry.ts
@@ -168,6 +168,7 @@ import CircularProgressDemo from "@/app/content/ui/circular-progress";
import CircularProgressWithTextDemo from "@/app/content/ui/circular-progress-text";
import CircularProgressVariantsDemo from "@/app/content/ui/circular-progress-variants";
import EditableDemo from "@/app/content/ui/editable";
+import EditableErrorDemo from "@/app/content/ui/editable-error";
import EditableTextareaDemo from "@/app/content/ui/editable-textarea";
import FieldDemo from "@/app/content/ui/field";
import FieldCheckboxDemo from "@/app/content/ui/field-checkbox";
@@ -184,6 +185,11 @@ import FieldWithSeparatorDemo from "@/app/content/ui/field-separator";
import FieldSmallDemo from "@/app/content/ui/field-small";
import FieldSwitchDemo from "@/app/content/ui/field-switch";
import FieldTextareaDemo from "@/app/content/ui/field-textarea";
+import FilterDemo from "@/app/content/ui/filter";
+import FilterInputDemo from "@/app/content/ui/filter-input";
+import FilterMultiSelectDemo from "@/app/content/ui/filter-multi-select";
+import FilterSingleSelectDemo from "@/app/content/ui/filter-single-select";
+import FilterToggleDemo from "@/app/content/ui/filter-toggle";
import InputGroupDemo from "@/app/content/ui/input-group";
import InputGroupDropdownDemo from "@/app/content/ui/input-group-dropdown";
import InputGroupSearchDemo from "@/app/content/ui/input-group-search";
@@ -202,11 +208,6 @@ import TimelineDemo from "@/app/content/ui/timeline";
import TimelineConnectorVariantsDemo from "@/app/content/ui/timeline-connector-variants";
import TimelineSizesDemo from "@/app/content/ui/timeline-sizes";
import TimelineVariantsDemo from "@/app/content/ui/timeline-variants";
-import FilterDemo from "@/app/content/ui/filter";
-import FilterInputDemo from "@/app/content/ui/filter-input";
-import FilterSingleSelectDemo from "@/app/content/ui/filter-single-select";
-import FilterMultiSelectDemo from "@/app/content/ui/filter-multi-select";
-import FilterToggleDemo from "@/app/content/ui/filter-toggle";
export interface DocsiteRegistryEntry {
name: string;
@@ -606,6 +607,11 @@ export const docsiteRegistry: Record = {
path: "src/app/content/ui/editable-textarea.tsx",
component: EditableTextareaDemo,
},
+ "editable-error": {
+ name: "editable-error",
+ path: "src/app/content/ui/editable-error.tsx",
+ component: EditableErrorDemo,
+ },
"empty-states-no-results": {
name: "empty-states-no-results",
path: "src/app/content/ui/empty-states-no-search-results.tsx",
@@ -736,31 +742,31 @@ export const docsiteRegistry: Record = {
path: "src/app/content/ui/field-input-types.tsx",
component: FieldInputTypesDemo,
},
- "filter": {
- name: "filter",
- path: "src/app/content/ui/filter.tsx",
- component: FilterDemo,
- },
- "filter-input": {
- name: "filter-input",
- path: "src/app/content/ui/filter-input.tsx",
- component: FilterInputDemo,
- },
- "filter-single-select": {
- name: "filter-single-select",
- path: "src/app/content/ui/filter-single-select.tsx",
- component: FilterSingleSelectDemo,
- },
- "filter-multi-select": {
- name: "filter-multi-select",
- path: "src/app/content/ui/filter-multi-select.tsx",
- component: FilterMultiSelectDemo,
- },
- "filter-toggle": {
- name: "filter-toggle",
- path: "src/app/content/ui/filter-toggle.tsx",
- component: FilterToggleDemo,
- },
+ filter: {
+ name: "filter",
+ path: "src/app/content/ui/filter.tsx",
+ component: FilterDemo,
+ },
+ "filter-input": {
+ name: "filter-input",
+ path: "src/app/content/ui/filter-input.tsx",
+ component: FilterInputDemo,
+ },
+ "filter-single-select": {
+ name: "filter-single-select",
+ path: "src/app/content/ui/filter-single-select.tsx",
+ component: FilterSingleSelectDemo,
+ },
+ "filter-multi-select": {
+ name: "filter-multi-select",
+ path: "src/app/content/ui/filter-multi-select.tsx",
+ component: FilterMultiSelectDemo,
+ },
+ "filter-toggle": {
+ name: "filter-toggle",
+ path: "src/app/content/ui/filter-toggle.tsx",
+ component: FilterToggleDemo,
+ },
icon: {
name: "icon",
path: "src/app/content/ui/icon-component.tsx",
diff --git a/src/lib/right-sidebar-metadata.ts b/src/lib/right-sidebar-metadata.ts
index dcab8df7..c35c448d 100644
--- a/src/lib/right-sidebar-metadata.ts
+++ b/src/lib/right-sidebar-metadata.ts
@@ -337,7 +337,10 @@ export const rightSidebarMetadata: Record = {
{
id: "examples",
title: "Examples",
- children: [{ id: "editable-textarea", title: "Textarea" }],
+ children: [
+ { id: "editable-textarea", title: "Textarea" },
+ { id: "editable-error", title: "Editable with errors" },
+ ],
},
],
},