- {!hideSearch && (
-
-
-
-
query}
- onChange={(
- event: React.ChangeEvent
- ) => {
- cancelRef.current?.removeAttribute("inert");
- cancelRef.current?.removeAttribute("aria-hidden");
- setQuery(event.target.value);
- }}
- autoComplete="off"
- onBlur={() => {
- cancelRef.current?.removeAttribute("inert");
- cancelRef.current?.removeAttribute("aria-hidden");
- }}
- placeholder="Search"
- />
- {
- cancelRef.current?.removeAttribute("inert");
- cancelRef.current?.removeAttribute("aria-hidden");
- }}
- >
- {loading ? (
-
+ )}
+
+
+ )}
+ body={({ isOpen: isPopoverOpen }) =>
+ isPopoverOpen && (
+
+ {!hideSearch && (
+
+
+
+ query}
+ onChange={(
+ event: React.ChangeEvent
+ ) => {
+ cancelRef.current?.removeAttribute("inert");
+ cancelRef.current?.removeAttribute("aria-hidden");
+ setQuery(event.target.value);
+ }}
+ autoComplete="off"
+ onBlur={() => {
+ cancelRef.current?.removeAttribute("inert");
+ cancelRef.current?.removeAttribute("aria-hidden");
+ }}
+ placeholder="Search"
/>
- ) : (
-
- )}
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
-
-
- )}
+ )}
-
- {groups.length === 0 ? (
-
- No results found
-
- ) : (
- groups.map((group) => (
-
- {group.group && !group.hideLabel && (
-
- {group.group}
-
- )}
- {group.items.slice(0, maxOptions).map((option, idx) => (
-
- `flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base ${
- focus ? "bg-surface-gray-3" : ""
- } ${
- (option as Option).disabled ? "opacity-50" : ""
- }`
- }
- >
- <>
-
- {(itemPrefix || multiple) && (
-
- {itemPrefix ? (
- itemPrefix(option as AutocompleteOption)
- ) : isOptionSelected(option as Option) ? (
-
- ) : (
-
+
+ {groups.length === 0 ? (
+
+ No results found
+
+ ) : (
+ groups.map((group) => (
+
+ {group.group && !group.hideLabel && (
+
+ {group.group}
+
+ )}
+ {group.items
+ .slice(0, maxOptions)
+ .map((option, idx) => (
+
+ `flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base ${
+ focus ? "bg-surface-gray-3" : ""
+ } ${
+ (option as Option).disabled
+ ? "opacity-50"
+ : ""
+ }`
+ }
+ >
+ <>
+
+ {(itemPrefix || multiple) && (
+
+ {itemPrefix ? (
+ itemPrefix(
+ option as AutocompleteOption
+ )
+ ) : isOptionSelected(
+ option as Option
+ ) ? (
+
+ ) : (
+
+ )}
+
)}
+
+ {getLabel(option)}
+
- )}
-
- {getLabel(option)}
-
-
-
- {itemSuffix && (
-
- {itemSuffix(option as Option)}
- {(option as Option)?.description && (
-
- {(option as Option).description}
+
+ {itemSuffix && (
+
+ {itemSuffix(option as Option)}
+ {(option as Option)?.description && (
+
+ {(option as Option).description}
+
+ )}
)}
-
- )}
- >
-
- ))}
-
- ))
- )}
-
-
- {showFooter && multiple && (
-
- {multiple ? (
-
- {!areAllOptionsSelected && (
-
- )}
- {areAllOptionsSelected && (
-
- )}
-
- ) : (
-
-
-
+ >
+
+ ))}
+
+ ))
)}
-
- )}
-
- )
- }
- />
- )}
-
+
+
+ {showFooter && multiple && (
+
+ {multiple ? (
+
+ {!areAllOptionsSelected && (
+
+ )}
+ {areAllOptionsSelected && (
+
+ )}
+
+ ) : (
+
+
+
+ )}
+
+ )}
+
+ )
+ }
+ />
+ )}
+
+
);
};
diff --git a/packages/frappe-ui-react/src/components/card/card.stories.tsx b/packages/frappe-ui-react/src/components/card/card.stories.tsx
new file mode 100644
index 00000000..b564fa6c
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/card/card.stories.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+} from "./card";
+import { Button } from "../button";
+
+export default {
+ title: "Components/Card",
+ component: Card,
+ tags: ["autodocs"],
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Card component for grouping related content. Includes header, title, description, content, and footer.",
+ },
+ },
+ },
+};
+
+export const Basic = () => (
+
+
+ Card Title
+ This is a card description.
+
+
+ This is the main content of the card.
+
+
+
+
+
+);
diff --git a/packages/frappe-ui-react/src/components/card/card.tsx b/packages/frappe-ui-react/src/components/card/card.tsx
new file mode 100644
index 00000000..55d0127c
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/card/card.tsx
@@ -0,0 +1,69 @@
+import * as React from "react";
+
+export const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+export const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+export const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+export const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+export const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+export const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
diff --git a/packages/frappe-ui-react/src/components/comments/comment.stories.tsx b/packages/frappe-ui-react/src/components/comments/comment.stories.tsx
new file mode 100644
index 00000000..a6066531
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/comment.stories.tsx
@@ -0,0 +1,75 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import Comments from './comments';
+import type { CommentData } from './types';
+
+const meta: Meta = {
+ title: 'Components/Comment System',
+ component: Comments,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+const initialComments: CommentData[] = [
+ {
+ id: 1,
+ author: {
+ name: 'Kanchan Chauhan',
+ avatarUrl: 'https://i.pravatar.cc/40?img=1',
+ },
+ timestamp: '1 month ago',
+ text: 'Hi @Devarshi Sathiya, did you check the assignment sent over by the candidate?',
+ replies: [
+ {
+ id: 2,
+ author: {
+ name: 'Devarshi Sathiya',
+ avatarUrl: 'https://i.pravatar.cc/40?img=2',
+ },
+ timestamp: '1 month ago',
+ text: 'Minor Update: Seems the assignment submission was not made properly adhering the guidelines, have asked the candidate to use our portal instead.',
+ replies: [
+ {
+ id: 3,
+ author: {
+ name: 'Niraj Gautam',
+ avatarUrl: 'https://i.pravatar.cc/40?img=3',
+ },
+ timestamp: '1 month ago',
+ text: 'Sure, works for me. Thanks!',
+ replies: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 4,
+ author: {
+ name: 'Niraj Gautam',
+ avatarUrl: 'https://i.pravatar.cc/40?img=3',
+ },
+ timestamp: '1 month ago',
+ text: 'Hey @Devarshi Sathiya, can you tell me what happened to my assignment submission? Seems to be pending for a while.',
+ replies: [
+ {
+ id: 5,
+ author: {
+ name: 'Devarshi Sathiya',
+ avatarUrl: 'https://i.pravatar.cc/40?img=2',
+ },
+ timestamp: '1 month ago',
+ text: "Yes, I am currently checking it. It seems to be well done, I'll submit over the full report by the end of the day.",
+ replies: [],
+ },
+ ],
+ },
+];
+
+export const FullThread: StoryObj = {
+ name: 'Full Interactive Comment Thread',
+ render: () => ,
+};
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/comments/comment.tsx b/packages/frappe-ui-react/src/components/comments/comment.tsx
new file mode 100644
index 00000000..ab7d74f4
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/comment.tsx
@@ -0,0 +1,123 @@
+import React, { useCallback, useState } from "react";
+import CommentForm from "./commentForm";
+import type { CommentData } from "./types";
+import { Avatar } from "../avatar";
+import { Button } from "../button";
+import { MessageSquareText, ReplyIcon } from "lucide-react";
+
+interface CommentProps {
+ comment: CommentData;
+ onAddReply: (parentId: number, text: string) => void;
+ handleEditComment: (id: number, text: string) => void;
+}
+
+function parseMentions(text: string): React.ReactNode[] {
+ const mentionRegex = /(@\w+\s\w+)/g;
+ const parts = text.split(mentionRegex);
+
+ return parts.map((part, index) =>
+ mentionRegex.test(part) ? (
+
+ {part}
+
+ ) : (
+ <>
+
+ >
+ )
+ );
+}
+
+function Comment({ comment, onAddReply, handleEditComment }: CommentProps) {
+ const [isReplying, setIsReplying] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+
+ const handleReplySubmit = useCallback((text: string): void => {
+ if (isReplying) {
+ onAddReply(comment.id, text);
+ setIsReplying(false);
+ }else{
+ handleEditComment(comment.id, text);
+ setIsEditing(false);
+ }
+ }, [comment.id, handleEditComment, isReplying, onAddReply]);
+
+ return (
+
+
+
+
+
+ {comment.author.name} commented
+ ยท {comment.timestamp}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {parseMentions(comment.text)}
+
+
+
+
+
+ {isReplying && (
+
+
+
+ )}
+
+ {isEditing && (
+
+
+
+ )}
+
+ {comment.replies &&
+ comment.replies.length > 0 &&
+ comment.replies.map((reply, index) => (
+
+ ))}
+
+ );
+}
+
+export default Comment;
diff --git a/packages/frappe-ui-react/src/components/comments/commentForm.tsx b/packages/frappe-ui-react/src/components/comments/commentForm.tsx
new file mode 100644
index 00000000..1c75a58b
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/commentForm.tsx
@@ -0,0 +1,45 @@
+import React, { useCallback, useState } from 'react';
+import TextEditor from '../textEditor';
+interface CommentFormProps {
+ onSubmit: (text: string) => void;
+ buttonText: string;
+ value?: string;
+}
+
+function CommentForm({ onSubmit, buttonText, value }: CommentFormProps) {
+ const [text, setText] = useState(value || '');
+ const isButtonDisabled = text.length === 0;
+
+ const handleSubmit = useCallback((e: React.FormEvent): void => {
+ e.preventDefault();
+ if (isButtonDisabled){
+ return;
+ }
+ onSubmit(text);
+ setText('');
+ }, [isButtonDisabled, onSubmit, text]);
+
+ return (
+
+ );
+}
+
+export default CommentForm;
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/comments/comments.tsx b/packages/frappe-ui-react/src/components/comments/comments.tsx
new file mode 100644
index 00000000..01ed2fce
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/comments.tsx
@@ -0,0 +1,138 @@
+import { useCallback, useState } from "react";
+import { MessageCircle } from "lucide-react";
+
+import Comment from "./comment";
+import CommentForm from "./commentForm";
+import type { CommentData } from "./types";
+
+const CURRENT_USER = {
+ name: "Current User",
+ avatarUrl: "https://i.pravatar.cc/40?img=4",
+};
+
+function addReplyToTree(
+ nodes: CommentData[],
+ parentId: number,
+ newReply: CommentData
+): CommentData[] {
+ return nodes.map((node) => {
+ if (node.id === parentId) {
+ return {
+ ...node,
+ replies: [...node.replies, newReply],
+ };
+ }
+
+ if (node.replies && node.replies.length > 0) {
+ return {
+ ...node,
+ replies: addReplyToTree(node.replies, parentId, newReply),
+ };
+ }
+ return node;
+ });
+}
+type CommentsProp = {
+ initialComments: CommentData[];
+ onCommentAdded?: (comment: CommentData) => void;
+ onCommentEdited?: (comment: CommentData) => void;
+ onCommentReplied?: (comment: CommentData, parentId: number) => void;
+};
+
+function Comments({
+ initialComments = [],
+ onCommentAdded,
+ onCommentEdited,
+ onCommentReplied,
+}: CommentsProp) {
+ const [comments, setComments] = useState(initialComments);
+
+ const handleAddComment = useCallback(
+ (text: string): void => {
+ const newComment: CommentData = {
+ id: Date.now(),
+ author: CURRENT_USER,
+ timestamp: "Just now",
+ text: text,
+ replies: [],
+ };
+ setComments([...comments, newComment]);
+ if (onCommentAdded) {
+ onCommentAdded(newComment);
+ }
+ },
+ [comments, onCommentAdded]
+ );
+
+ const handleEditComment = useCallback(
+ (id: number, text: string): void => {
+ setComments((currentComments) => {
+ return currentComments.map((comment) => {
+ if (comment.id === id) {
+ if (onCommentEdited) {
+ onCommentEdited({ ...comment, text });
+ }
+ return { ...comment, text };
+ }
+ return comment;
+ });
+ });
+ },
+ [onCommentEdited]
+ );
+
+ const handleAddReply = useCallback(
+ (parentId: number, text: string): void => {
+ const newReply: CommentData = {
+ id: Date.now(),
+ author: CURRENT_USER,
+ timestamp: "Just now",
+ text: text,
+ replies: [],
+ };
+
+ if (onCommentReplied) {
+ onCommentReplied(newReply, parentId);
+ }
+
+ setComments((currentComments) =>
+ addReplyToTree(currentComments, parentId, newReply)
+ );
+ },
+ [onCommentReplied]
+ );
+
+ return (
+
+
Comments
+
+ {comments.length === 0 && (
+
+ )}
+
+
+ {comments.map((comment, index) => (
+
+ ))}
+
+
+ );
+}
+
+export default Comments;
diff --git a/packages/frappe-ui-react/src/components/comments/index.ts b/packages/frappe-ui-react/src/components/comments/index.ts
new file mode 100644
index 00000000..e4fd5ce8
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/index.ts
@@ -0,0 +1,2 @@
+export { default as Comments } from "./comments";
+export * from './types';
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/comments/types.ts b/packages/frappe-ui-react/src/components/comments/types.ts
new file mode 100644
index 00000000..0c774495
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/comments/types.ts
@@ -0,0 +1,12 @@
+export interface User {
+ name: string;
+ avatarUrl: string;
+}
+
+export interface CommentData {
+ id: number;
+ author: User;
+ timestamp: string;
+ text: string;
+ replies: CommentData[];
+}
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/confirmationDialog/confirmationDialog.stories.tsx b/packages/frappe-ui-react/src/components/confirmationDialog/confirmationDialog.stories.tsx
new file mode 100644
index 00000000..66a84211
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/confirmationDialog/confirmationDialog.stories.tsx
@@ -0,0 +1,136 @@
+import React, { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { ConfirmationDialog } from './';
+import { Button } from '../button';
+
+
+const meta: Meta = {
+ title: 'Components/ConfirmationDialog',
+ component: ConfirmationDialog,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ isOpen: {
+ control: 'boolean',
+ description: 'Controls if the dialog is open or closed.',
+ table: {
+ category: 'State',
+ },
+ },
+ isLoading: {
+ control: 'boolean',
+ description: 'Shows a loading spinner on the delete button.',
+ table: {
+ category: 'State',
+ },
+ },
+ title: {
+ control: 'text',
+ description: 'The main title of the dialog.',
+ table: {
+ category: 'Content',
+ },
+ },
+ description: {
+ control: 'text',
+ description: 'The body text/description of the dialog.',
+ table: {
+ category: 'Content',
+ },
+ },
+ onDelete: {
+ action: 'onDelete',
+ description: 'Callback fired when the "Delete" button is clicked.',
+ table: {
+ category: 'Events',
+ },
+ },
+ onCancel: {
+ action: 'onCancel',
+ description:
+ 'Callback fired when the "Cancel" button is clicked or the dialog is closed (e.g., by clicking overlay).',
+ table: {
+ category: 'Events',
+ },
+ },
+ },
+ args: {
+ isOpen: false,
+ isLoading: false,
+ title: 'Are you absolutely sure?',
+ description:
+ 'This action cannot be undone. This will permanently delete this item and all of its associated data.',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ name: 'Interactive Example',
+ render: (args) => {
+ const [isOpen, setIsOpen] = useState(args.isOpen || false);
+ const [isLoading, setIsLoading] = useState(args.isLoading || false);
+
+ const handleCancel = () => {
+ setIsOpen(false);
+ setIsLoading(false);
+ args.onCancel?.();
+ };
+
+ const handleDelete = () => {
+ setIsLoading(true);
+ args.onDelete?.();
+ setTimeout(() => {
+ setIsLoading(false);
+ setIsOpen(false);
+ console.log('Item deleted');
+ }, 2000);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+
+ args: {
+ isOpen: false,
+ isLoading: false,
+ },
+};
+
+export const DefaultOpen: Story = {
+ name: 'Open',
+ args: {
+ isOpen: true,
+ isLoading: false,
+ title: 'Confirm Deletion',
+ description:
+ 'Are you sure you want to delete this resource? This process is irreversible.',
+ },
+};
+
+export const LoadingState: Story = {
+ name: 'Loading',
+ args: {
+ isOpen: true,
+ isLoading: true,
+ title: 'Deleting Resource...',
+ description:
+ 'Please wait while the resource is being deleted. This may take a moment.',
+ },
+};
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/confirmationDialog/index.tsx b/packages/frappe-ui-react/src/components/confirmationDialog/index.tsx
new file mode 100644
index 00000000..53f2fe9c
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/confirmationDialog/index.tsx
@@ -0,0 +1,70 @@
+/**
+ * External dependencies.
+ */
+import { LoaderCircle, Trash2, X } from "lucide-react";
+/**
+ * External dependencies.
+ */
+import { Button } from "../button";
+import { Dialog } from "../dialog";
+/**
+ * The resource delete allocation alert dialog.
+ *
+ * Why not use react-alert-dialog?
+ * The above package was creating issues for form dynamic field selection, also it has bugs in recent versions: https://github.com/shadmergeClassNames-ui/ui/issues/1655 so for now I have used dialog only.
+ *
+ * @param props.onDelete The function to be called when delete dialog is clicked.
+ * @param props.isOpen The state to open the dialog.
+ * @param props.isLoading The state to show the loader.
+ * @param props.onOpen The function to open the dialog.
+ * @param props.onCancel The function to cancel the dialog.
+ * @returns React.FC
+ */
+export const ConfirmationDialog = ({
+ onDelete,
+ isOpen,
+ isLoading,
+ onCancel,
+ title,
+ description,
+}: {
+ onDelete: () => void;
+ isOpen: boolean;
+ isLoading: boolean;
+ onOpen: () => void;
+ onCancel: () => void;
+ buttonClassName?: string;
+ title: string;
+ description: string;
+}) => {
+ return (
+
+ );
+};
diff --git a/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx b/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx
index c79b7934..a1ffb074 100644
--- a/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx
+++ b/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx
@@ -3,8 +3,8 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import Dialog from "./dialog";
import { Button } from "../button";
import { Dropdown } from "../dropdown";
-import { Autocomplete, AutocompleteOption } from "../autoComplete";
-import { DialogOptions } from "./types";
+import { Autocomplete, type AutocompleteOption } from "../autoComplete";
+import { type DialogOptions } from "./types";
const meta: Meta = {
title: "Components/Dialog",
diff --git a/packages/frappe-ui-react/src/components/dialog/types.ts b/packages/frappe-ui-react/src/components/dialog/types.ts
index 36da537e..dd1af43f 100644
--- a/packages/frappe-ui-react/src/components/dialog/types.ts
+++ b/packages/frappe-ui-react/src/components/dialog/types.ts
@@ -34,11 +34,13 @@ export interface DialogOptions {
name: string;
appearance?: "info" | "success" | "warning" | "danger";
};
+ extraDialogPositionClasses?: string;
actions?: DialogAction[];
}
export interface DialogProps {
open: boolean;
+ sheetClasses?: string;
onOpenChange: (open: boolean) => void;
options?: DialogOptions;
disableOutsideClickToClose?: boolean;
diff --git a/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx b/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx
index c0d0913d..179f76a9 100644
--- a/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx
+++ b/packages/frappe-ui-react/src/components/dropdown/dropdown.stories.tsx
@@ -3,7 +3,7 @@ import { action } from "storybook/actions";
import Dropdown from "./dropdown";
import { Button } from "../button";
-import { DropdownOptions } from "./types";
+import type { DropdownOptions } from "./types";
export default {
title: "Components/Dropdown",
diff --git a/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx b/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx
index e967fefa..4c88d0a9 100644
--- a/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx
+++ b/packages/frappe-ui-react/src/components/formControl/formControl.stories.tsx
@@ -156,11 +156,13 @@ export const Autocomplete: Story = {
render: (args) => {
const [value, setValue] = useState("");
return (
- setValue(_value)}
- />
+
+ setValue(_value)}
+ />
+
);
},
};
diff --git a/packages/frappe-ui-react/src/components/hoverCard/index.tsx b/packages/frappe-ui-react/src/components/hoverCard/index.tsx
new file mode 100644
index 00000000..cd33559d
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/hoverCard/index.tsx
@@ -0,0 +1,28 @@
+/*
+ * External dependencies.
+ */
+import { Root, Trigger, Content, type HoverCardContentProps } from "@radix-ui/react-hover-card";
+
+/**
+ * Internal dependencies.
+ */
+import { mergeClassNames } from "../../utils";
+
+const HoverCard = Root;
+
+const HoverCardTrigger = Trigger;
+
+const HoverCardContent = ({ className = "", align = "center", sideOffset = 4, ...props }: HoverCardContentProps, ref: React.Ref) => (
+
+);
+
+export { HoverCard, HoverCardTrigger, HoverCardContent };
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/index.ts b/packages/frappe-ui-react/src/components/index.ts
index dc7053e8..740334d6 100644
--- a/packages/frappe-ui-react/src/components/index.ts
+++ b/packages/frappe-ui-react/src/components/index.ts
@@ -47,3 +47,5 @@ export {
export { default as keyboardShortcut } from "./keyboardShortcut";
export { default as LoadingIndicator } from "./loadingIndicator";
export { default as LoadingText } from "./loadingText";
+export * from './sheet';
+export * from './comments';
diff --git a/packages/frappe-ui-react/src/components/listview/listHeader.tsx b/packages/frappe-ui-react/src/components/listview/listHeader.tsx
index 605f3ce3..c1f03207 100644
--- a/packages/frappe-ui-react/src/components/listview/listHeader.tsx
+++ b/packages/frappe-ui-react/src/components/listview/listHeader.tsx
@@ -35,7 +35,7 @@ const ListHeader: React.FC = ({ children }) => {
item={column}
lastItem={index === list.columns.length - 1}
onColumnWidthUpdated={(width: number) => {
- list.updateColumnWidth(index, width);
+ list.options.updateColumnWidth(index, width);
}}
/>
))}
diff --git a/packages/frappe-ui-react/src/components/listview/listprovider.tsx b/packages/frappe-ui-react/src/components/listview/listprovider.tsx
index 2c7bc539..5244b598 100644
--- a/packages/frappe-ui-react/src/components/listview/listprovider.tsx
+++ b/packages/frappe-ui-react/src/components/listview/listprovider.tsx
@@ -58,8 +58,9 @@ export const ListProvider: React.FC = ({
title: "No Data",
description: "No data available",
},
+ updateColumnWidth,
};
- }, [options]);
+ }, [options, updateColumnWidth]);
const showGroupedRows = useMemo(
() => rows.every((row) => row.group && row.rows && Array.isArray(row.rows)),
@@ -135,7 +136,6 @@ export const ListProvider: React.FC = ({
toggleAllRows,
emptyState: options.emptyState,
setColumns: () => {},
- updateColumnWidth,
},
}),
[
@@ -150,7 +150,6 @@ export const ListProvider: React.FC = ({
allRowsSelected,
toggleRow,
toggleAllRows,
- updateColumnWidth,
]
);
return (
diff --git a/packages/frappe-ui-react/src/components/sheet/index.tsx b/packages/frappe-ui-react/src/components/sheet/index.tsx
new file mode 100644
index 00000000..f640f822
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/sheet/index.tsx
@@ -0,0 +1,110 @@
+/**
+ * External dependencies.
+ */
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+
+/**
+ * Internal dependencies.
+ */
+import { mergeClassNames } from "../../utils";
+
+const sheetVariants = cva(
+ "fixed z-51 gap-4 bg-surface-white p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+);
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const Sheet = SheetPrimitive.Root;
+const SheetTrigger = SheetPrimitive.Trigger;
+const SheetClose = SheetPrimitive.Close;
+
+const SheetContent =({ side = "right", className, children, ...props }: SheetContentProps, ref?: React.Ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+);
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+
+const SheetTitle = ({ className, ...props }: SheetPrimitive.DialogTitleProps, ref?: React.Ref) => (
+
+);
+
+const SheetDescription = ({ className, ...props }: SheetPrimitive.DialogDescriptionProps, ref?: React.Ref) => (
+
+);
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/sheet/sheet.stories.tsx b/packages/frappe-ui-react/src/components/sheet/sheet.stories.tsx
new file mode 100644
index 00000000..c9716f0e
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/sheet/sheet.stories.tsx
@@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import React from 'react';
+
+import {
+ Sheet,
+ SheetTrigger,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+ SheetClose,
+} from './';
+import { Button } from '../button';
+import FormLabel from '../formLabel';
+import { TextInput } from '../textInput';
+
+const meta: Meta = {
+ title: 'Components/Sheet',
+ component: SheetContent,
+ tags: ['autodocs'],
+ argTypes: {
+ side: {
+ control: 'radio',
+ options: ['top', 'bottom', 'left', 'right'],
+ description: 'Which side the sheet appears from.',
+ },
+ },
+ parameters: {
+ component: null,
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ name: 'Default (Right Side)',
+ args: {
+ side: 'right',
+ },
+ render: (args) => (
+
+
+
+
+
+
+ Edit Profile
+
+ Make changes to your profile here. Click save when you're done.
+
+
+
+
+
+
+
+
+
+
+ ),
+};
+
+
+export const LeftSide: Story = {
+ ...Default,
+ name: 'From Left',
+ args: {
+ side: 'left',
+ },
+};
+
+export const TopSide: Story = {
+ ...Default,
+ name: 'From Top',
+ args: {
+ side: 'top',
+ },
+};
+
+export const BottomSide: Story = {
+ ...Default,
+ name: 'From Bottom',
+ args: {
+ side: 'bottom',
+ },
+};
\ No newline at end of file
diff --git a/packages/frappe-ui-react/src/components/taskStatus/index.tsx b/packages/frappe-ui-react/src/components/taskStatus/index.tsx
new file mode 100644
index 00000000..22373ed2
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/taskStatus/index.tsx
@@ -0,0 +1,39 @@
+const statusVariants: Record = {
+ Open: "bg-blue-100 text-blue-500 hover:bg-blue-200",
+ Working: "bg-orange-500/20 text-orange-500 hover:bg-orange-500/30",
+ "Pending Review": "bg-orange-100 text-orange-400 hover:bg-orange-500/20",
+ Overdue: "bg-red-500/20 text-red-500 hover:bg-red-500/20",
+ Template: "bg-slate-200 text-slate-900 hover:bg-slate-200",
+ Completed: "bg-green-600/20 text-green-600 hover:bg-green-600/20",
+ Cancelled: "bg-red-500/20 text-red-500 hover:bg-red-500/20",
+};
+
+export type TaskStatusProps =
+ | "Open"
+ | "Working"
+ | "Pending Review"
+ | "Overdue"
+ | "Template"
+ | "Completed"
+ | "Cancelled";
+
+interface TaskStatusComponentProps {
+ status: TaskStatusProps;
+}
+
+const TaskStatus = ({ status }: TaskStatusComponentProps) => {
+ const variantClass = statusVariants[status] || statusVariants["Open"];
+
+ return (
+
+ {status}
+
+ );
+};
+
+export default TaskStatus;
diff --git a/packages/frappe-ui-react/src/components/taskStatus/taskStatus.stories.tsx b/packages/frappe-ui-react/src/components/taskStatus/taskStatus.stories.tsx
new file mode 100644
index 00000000..012fde5e
--- /dev/null
+++ b/packages/frappe-ui-react/src/components/taskStatus/taskStatus.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import TaskStatus, { type TaskStatusProps } from "./index";
+
+export default {
+ title: "Components/TaskStatus",
+ component: TaskStatus,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {
+ status: {
+ control: {
+ type: "select",
+ options: [
+ "Open",
+ "Working",
+ "Pending Review",
+ "Overdue",
+ "Template",
+ "Completed",
+ "Cancelled",
+ ],
+ },
+ description: "Status variant for the task",
+ },
+ },
+} as Meta;
+
+const Template: StoryObj<{ status: TaskStatusProps }> = {
+ render: (args) => (
+
+
+
+ ),
+};
+
+export const Open = {
+ ...Template,
+ args: { status: "Open" },
+};
+export const Working = {
+ ...Template,
+ args: { status: "Working" },
+};
+export const PendingReview = {
+ ...Template,
+ args: { status: "Pending Review" },
+};
+export const Overdue = {
+ ...Template,
+ args: { status: "Overdue" },
+};
+export const TemplateStatus = {
+ ...Template,
+ args: { status: "Template" },
+};
+export const Completed = {
+ ...Template,
+ args: { status: "Completed" },
+};
+export const Cancelled = {
+ ...Template,
+ args: { status: "Cancelled" },
+};
+
+export const AllVariants = {
+ render: () => (
+
+ {[
+ "Open",
+ "Working",
+ "Pending Review",
+ "Overdue",
+ "Template",
+ "Completed",
+ "Cancelled",
+ ].map((status) => (
+
+ ))}
+
+ ),
+};
diff --git a/packages/frappe-ui-react/src/components/textEditor/index.tsx b/packages/frappe-ui-react/src/components/textEditor/index.tsx
index ff4a03ab..bc2c9c83 100644
--- a/packages/frappe-ui-react/src/components/textEditor/index.tsx
+++ b/packages/frappe-ui-react/src/components/textEditor/index.tsx
@@ -25,6 +25,7 @@ export interface TextEditorProps extends ReactQuill.ReactQuillProps {
onChange: (value: string) => void;
value?: string;
placeholder?: string;
+ editingAreaRef?: React.RefObject;
}
Quill.register("modules/imageResize", ImageResize);
diff --git a/packages/frappe-ui-react/src/components/textarea/textarea.tsx b/packages/frappe-ui-react/src/components/textarea/textarea.tsx
index 453676eb..f33a36b6 100644
--- a/packages/frappe-ui-react/src/components/textarea/textarea.tsx
+++ b/packages/frappe-ui-react/src/components/textarea/textarea.tsx
@@ -16,6 +16,8 @@ const Textarea = forwardRef(
rows = 3,
htmlId,
placeholder,
+ extraClasses = '',
+ ...props
},
ref
) => {
@@ -62,7 +64,7 @@ const Textarea = forwardRef(
const textColor = disabled ? "text-ink-gray-5" : "text-ink-gray-8";
- return `resize-y transition-colors w-full block outline-none ${sizeClasses} ${paddingClasses} ${variantClasses} ${textColor}`;
+ return `resize-y transition-colors w-full block outline-none ${sizeClasses} ${paddingClasses} ${variantClasses} ${textColor} ${extraClasses}`;
}, [size, disabled, variant]);
const labelClasses = useMemo(() => {
@@ -117,7 +119,8 @@ const Textarea = forwardRef(
id={htmlId}
value={value}
onChange={handleChange}
- data-testid="textarea"
+ data-testid="textarea"
+ {...props}
/>
);
diff --git a/packages/frappe-ui-react/src/components/textarea/types.ts b/packages/frappe-ui-react/src/components/textarea/types.ts
index 05ab9b69..4d6af6e9 100644
--- a/packages/frappe-ui-react/src/components/textarea/types.ts
+++ b/packages/frappe-ui-react/src/components/textarea/types.ts
@@ -12,5 +12,7 @@ export interface TextareaProps {
debounce?: number;
rows?: number;
onChange?: (event: React.ChangeEvent