diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.stories.tsx b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.stories.tsx new file mode 100644 index 000000000000..0327ddedc158 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.stories.tsx @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ +import type { Meta, StoryObj } from "@storybook/react"; + +import { DismissibleTab } from "."; + +const meta: Meta = { + title: "ADS/Components/Dismissible Tab", + component: DismissibleTab, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + isActive: true, + dataTestId: "t--dismissible-tab", + children: "Dismissible tab", + }, +}; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.styles.ts b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.styles.ts new file mode 100644 index 000000000000..42abfbe698c9 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.styles.ts @@ -0,0 +1,55 @@ +import styled from "styled-components"; + +import { Button as ADSButton } from ".."; + +export const Tab = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + gap: var(--ads-v2-spaces-2); + height: 100%; + font-size: 12px; + color: var(--ads-v2-color-fg); + cursor: pointer; + + border-top-left-radius: var(--ads-v2-border-radius); + border-top-right-radius: var(--ads-v2-border-radius); + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-top: 3px solid transparent; + + padding: var(--ads-v2-spaces-3); + padding-top: 6px; + + &.active { + background: var(--ads-v2-colors-control-field-default-bg); + border-top-color: var(--ads-v2-color-bg-brand); + border-left-color: var(--ads-v2-color-border-muted); + border-right-color: var(--ads-v2-color-border-muted); + + span { + font-weight: var(--ads-v2-font-weight-bold); + } + } + + & > .tab-close { + opacity: 0; + transition: opacity 0.2s ease; + } + + &:hover > .tab-close, + &:focus-within > .tab-close, + &.active > .tab-close { + opacity: 1; + } +`; + +export const CloseButton = styled(ADSButton)` + border-radius: 2px; + cursor: pointer; + padding: var(--ads-v2-spaces-1); + max-width: 16px; + max-height: 16px; +`; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx new file mode 100644 index 000000000000..78a05a887bbf --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +import clsx from "classnames"; + +import { Icon } from ".."; + +import * as Styled from "./DismissibleTab.styles"; +import { DATA_TEST_ID } from "./constants"; + +import type { DismissibleTabProps } from "./DismissibleTab.types"; + +export const DismissibleTab = ({ + children, + dataTestId, + isActive, + onClick, + onClose, + onDoubleClick, +}: DismissibleTabProps) => { + return ( + + {children} + + + + + ); +}; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.types.ts b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.types.ts new file mode 100644 index 000000000000..909dafab3177 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.types.ts @@ -0,0 +1,10 @@ +import type React from "react"; + +export interface DismissibleTabProps { + children: React.ReactNode; + dataTestId?: string; + isActive: boolean; + onClick: () => void; + onClose: (e: React.MouseEvent) => void; + onDoubleClick?: () => void; +} diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/constants.ts b/app/client/packages/design-system/ads/src/DismissibleTab/constants.ts new file mode 100644 index 000000000000..71d29d3f7c48 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/constants.ts @@ -0,0 +1,3 @@ +export const DATA_TEST_ID = { + CLOSE_BUTTON: "t--tab-close-btn", +}; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/index.ts b/app/client/packages/design-system/ads/src/DismissibleTab/index.ts new file mode 100644 index 000000000000..f00320299ac8 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/index.ts @@ -0,0 +1,2 @@ +export { DismissibleTab } from "./DismissibleTab"; +export type { DismissibleTabProps } from "./DismissibleTab.types"; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.stories.tsx b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.stories.tsx new file mode 100644 index 000000000000..7dfe5b8289e3 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.stories.tsx @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { EditableDismissibleTab } from "."; +import styled from "styled-components"; + +import { Icon } from "../.."; + +const meta: Meta = { + title: "ADS/Templates/Editable Dismissible Tab", + component: EditableDismissibleTab, +}; + +const EntityIcon = styled.div` + height: 18px; + width: 18px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + + svg, + img { + height: 100%; + width: 100%; + } +`; + +const JSIcon = () => { + return ( + + + + ); +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + isActive: true, + dataTestId: "t--dismissible-tab", + icon: JSIcon(), + name: "Hello", + + onNameSave: console.log, + validateName: (name: string) => + name.length < 3 ? "Name must be at least 3 characters" : null, + }, +}; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx new file mode 100644 index 000000000000..9ed6cc20e50e --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { noop } from "lodash"; +import { useBoolean } from "usehooks-ts"; + +import { DismissibleTab } from "../.."; +import { EditableEntityName } from ".."; + +import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types"; + +export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => { + const { + dataTestId, + icon, + isActive, + isEditable = true, + isLoading, + name, + onClick, + onClose, + onNameSave, + validateName, + } = props; + + const { + setFalse: exitEditMode, + setTrue: enterEditMode, + value: isEditing, + } = useBoolean(false); + + const handleDoubleClick = isEditable ? enterEditMode : noop; + + return ( + + + + ); +}; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts new file mode 100644 index 000000000000..2ee09e171053 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/EditableDismissibleTab.types.ts @@ -0,0 +1,14 @@ +import type React from "react"; + +export interface EditableDismissibleTabProps { + dataTestId?: string; + icon: React.ReactNode; + isActive: boolean; + isEditable?: boolean; + isLoading: boolean; + name: string; + onClick: () => void; + onClose: () => void; + onNameSave: (name: string) => void; + validateName: (name: string) => string | null; +} diff --git a/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/index.ts b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/index.ts new file mode 100644 index 000000000000..5c3ff232bae3 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableDismissibleTab/index.ts @@ -0,0 +1 @@ +export { EditableDismissibleTab } from "./EditableDismissibleTab"; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.stories.tsx b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.stories.tsx new file mode 100644 index 000000000000..48c9a769c093 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.stories.tsx @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import styled from "styled-components"; + +import { Icon } from "../.."; +import { EditableEntityName } from "."; + +const EntityIcon = styled.div` + height: 18px; + width: 18px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + + svg, + img { + height: 100%; + width: 100%; + } +`; + +const JSIcon = () => { + return ( + + + + ); +}; + +const meta: Meta = { + title: "ADS/Templates/Editable Entity Name", + component: EditableEntityName, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + name: "Hello", + onNameSave: console.log, + onExitEditing: console.log, + icon: JSIcon(), + inputTestId: "t--editable-name", + isEditing: true, + isLoading: false, + validateName: (name: string) => + name.length < 3 ? "Name must be at least 3 characters" : null, + }, +}; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.styles.ts b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.styles.ts new file mode 100644 index 000000000000..171b508e6b8b --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.styles.ts @@ -0,0 +1,29 @@ +import styled from "styled-components"; +import { Text as ADSText } from "../../Text"; + +export const Root = styled.div` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: start; + align-items: center; + gap: var(--ads-v2-spaces-2); +`; + +export const Text = styled(ADSText)` + min-width: 3ch; + bottom: -0.5px; +`; + +export const IconContainer = styled.div` + height: 12px; + width: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img { + width: 12px; + } +`; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.tsx b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.tsx new file mode 100644 index 000000000000..fa26160d2a37 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.tsx @@ -0,0 +1,64 @@ +import React, { useMemo } from "react"; + +import { Spinner, Tooltip } from "../.."; +import { useEditableText } from "../../__hooks__"; + +import * as Styled from "./EditableEntityName.styles"; + +import type { EditableEntityNameProps } from "./EditableEntityName.types"; + +export const EditableEntityName = ({ + icon, + inputTestId, + isEditing, + isLoading = false, + name, + onExitEditing, + onNameSave, + validateName, +}: EditableEntityNameProps) => { + const [ + inputRef, + editableName, + validationError, + handleKeyUp, + handleTitleChange, + ] = useEditableText(isEditing, name, onExitEditing, validateName, onNameSave); + + const inputProps = useMemo( + () => ({ + ["data-testid"]: inputTestId, + onKeyUp: handleKeyUp, + onChange: handleTitleChange, + autoFocus: true, + style: { + paddingTop: 4, + paddingBottom: 4, + left: -1, + top: -5, + }, + }), + [handleKeyUp, handleTitleChange, inputTestId], + ); + + return ( + + {isLoading ? ( + + ) : ( + {icon} + )} + + + {editableName} + + + + ); +}; diff --git a/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.types.ts b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.types.ts new file mode 100644 index 000000000000..b092f01b41be --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/EditableEntityName.types.ts @@ -0,0 +1,12 @@ +import type React from "react"; + +export interface EditableEntityNameProps { + icon: React.ReactNode; + inputTestId?: string; + isEditing: boolean; + isLoading?: boolean; + name: string; + onExitEditing: () => void; + onNameSave: (name: string) => void; + validateName: (name: string) => string | null; +} diff --git a/app/client/packages/design-system/ads/src/Templates/EditableEntityName/index.ts b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/index.ts new file mode 100644 index 000000000000..0230a869ac95 --- /dev/null +++ b/app/client/packages/design-system/ads/src/Templates/EditableEntityName/index.ts @@ -0,0 +1 @@ +export { EditableEntityName } from "./EditableEntityName"; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx index cf5191006712..066ff8cf0599 100644 --- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx @@ -5,7 +5,7 @@ import { Tooltip } from "../../../Tooltip"; import type { EntityItemProps } from "./EntityItem.types"; import { EntityEditableName } from "./EntityItem.styles"; -import { useEditableText } from "../Editable"; +import { useEditableText } from "../../../__hooks__/useEditableText"; import clx from "classnames"; export const EntityItem = (props: EntityItemProps) => { diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts index bae3358c1a06..82b07ac50f5a 100644 --- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts +++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts @@ -6,6 +6,6 @@ export { EmptyState } from "./EmptyState"; export { NoSearchResults } from "./NoSearchResults"; export * from "./ExplorerContainer"; export * from "./EntityItem"; -export { useEditableText } from "./Editable"; +export { useEditableText } from "../../__hooks__/useEditableText"; export * from "./EntityGroupsList"; export * from "./EntityListTree"; diff --git a/app/client/packages/design-system/ads/src/Templates/index.ts b/app/client/packages/design-system/ads/src/Templates/index.ts index e8258f9d1b5f..4b7b197c824f 100644 --- a/app/client/packages/design-system/ads/src/Templates/index.ts +++ b/app/client/packages/design-system/ads/src/Templates/index.ts @@ -1,3 +1,5 @@ export * from "./IDEHeader"; export * from "./EntityExplorer"; export * from "./Sidebar"; +export * from "./EditableEntityName"; +export * from "./EditableDismissibleTab"; diff --git a/app/client/packages/design-system/ads/src/__hooks__/index.ts b/app/client/packages/design-system/ads/src/__hooks__/index.ts new file mode 100644 index 000000000000..b75011d84cf4 --- /dev/null +++ b/app/client/packages/design-system/ads/src/__hooks__/index.ts @@ -0,0 +1,2 @@ +export { useDOMRef } from "./useDomRef"; +export { useEditableText } from "./useEditableText"; diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/index.ts b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/index.ts similarity index 100% rename from app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/index.ts rename to app/client/packages/design-system/ads/src/__hooks__/useEditableText/index.ts diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.test.tsx b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.test.tsx similarity index 99% rename from app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.test.tsx rename to app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.test.tsx index d314a8e193cb..628e7aceaabd 100644 --- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.test.tsx +++ b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.test.tsx @@ -1,8 +1,9 @@ import React from "react"; import { renderHook, act } from "@testing-library/react-hooks"; -import { useEditableText } from "./useEditableText"; import { fireEvent, render } from "@testing-library/react"; -import { Text } from "../../.."; +import { Text } from "../.."; + +import { useEditableText } from "./useEditableText"; describe("useEditableText", () => { const mockExitEditing = jest.fn(); diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.ts b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.ts similarity index 99% rename from app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.ts rename to app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.ts index 59b3d4e26784..80b95891d40e 100644 --- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/useEditableText.ts +++ b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/useEditableText.ts @@ -7,8 +7,10 @@ import { useRef, type RefObject, } from "react"; + import { usePrevious } from "@mantine/hooks"; import { useEventCallback, useEventListener } from "usehooks-ts"; + import { normaliseName } from "./utils"; export function useEditableText( diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/utils.ts b/app/client/packages/design-system/ads/src/__hooks__/useEditableText/utils.ts similarity index 100% rename from app/client/packages/design-system/ads/src/Templates/EntityExplorer/Editable/utils.ts rename to app/client/packages/design-system/ads/src/__hooks__/useEditableText/utils.ts diff --git a/app/client/packages/design-system/ads/src/index.ts b/app/client/packages/design-system/ads/src/index.ts index 4a9cee88b871..e6e603b714a4 100644 --- a/app/client/packages/design-system/ads/src/index.ts +++ b/app/client/packages/design-system/ads/src/index.ts @@ -1,14 +1,16 @@ import "./__theme__/default/index.css"; -export * from "./AnnouncementPopover"; export * from "./AnnouncementModal"; +export * from "./AnnouncementPopover"; export * from "./Avatar"; -export * from "./Button"; +export * from "./Badge"; export * from "./Banner"; +export * from "./Button"; export * from "./Callout"; export * from "./Checkbox"; export * from "./Collapsible"; export * from "./DatePicker"; +export * from "./DismissibleTab"; export * from "./Divider"; export * from "./Flex"; export * from "./FormControl"; @@ -31,10 +33,9 @@ export * from "./Switch"; export * from "./Tab"; export * from "./Table"; export * from "./Tag"; +export * from "./Templates"; export * from "./Text"; export * from "./Toast"; export * from "./ToggleButton"; -export * from "./Tooltip"; export * from "./ToggleButtonGroup"; -export * from "./Templates"; -export * from "./Badge"; +export * from "./Tooltip"; diff --git a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx index 8aeab5dee079..9682f927a1a3 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx +++ b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx @@ -57,7 +57,7 @@ const _Button = (props: ButtonProps, ref: ForwardedRef) => { {isLoading && ( - + {loadingText} )} diff --git a/app/client/packages/design-system/widgets/src/components/Button/src/types.ts b/app/client/packages/design-system/widgets/src/components/Button/src/types.ts index fd181f5e16ff..82ebeb68d55c 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Button/src/types.ts @@ -40,7 +40,7 @@ export interface ButtonProps extends HeadlessButtonProps { /** Size of the button * @default medium */ - size?: Omit; + size?: Exclude; /** Indicates if the button should be disabled when the form is invalid */ disableOnInvalidForm?: boolean; /** Indicates if the button should reset the form when clicked */ diff --git a/app/client/packages/design-system/widgets/src/components/Button/stories/Button.stories.tsx b/app/client/packages/design-system/widgets/src/components/Button/stories/Button.stories.tsx index 8525882f1990..b456ff828805 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/stories/Button.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Button/stories/Button.stories.tsx @@ -1,6 +1,13 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Button, Flex, BUTTON_VARIANTS, COLORS, SIZES } from "@appsmith/wds"; +import { + Button, + Flex, + BUTTON_VARIANTS, + COLORS, + SIZES, + type ButtonProps, +} from "@appsmith/wds"; import { objectKeys } from "@appsmith/utils"; /** @@ -59,15 +66,21 @@ export const Sizes: Story = { render: () => ( - {Object.keys(SIZES) - .filter((size) => !["large"].includes(size)) + {objectKeys(SIZES) + .filter( + (size): size is NonNullable => + !["large"].includes(size), + ) .map((size) => (