From ece99ba6256f6a24a50513c91d9e9133f317cc92 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 24 Mar 2026 16:48:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20editable=20componen?= =?UTF-8?q?ts=20to=20shared=20UI=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/SingleLineEditable.tsx | 106 ------------------ .../buttons/components/ButtonsItemNode.tsx | 2 +- .../inputs/cards/components/CardsItemNode.tsx | 16 +-- .../editor/components/EditableTypebotName.tsx | 2 +- .../folders/components/FolderButton.tsx | 2 +- .../components/nodes/group/GroupNode.tsx | 2 +- .../preview/components/VariablesDrawer.tsx | 2 +- .../publish/components/EditableUrl.tsx | 2 +- .../theme/api/handleSaveThemeTemplate.ts | 24 ++-- bun.lock | 4 +- packages/embeds/react/package.json | 5 +- .../ui}/src/components/MultiLineEditable.tsx | 61 +++++----- .../ui/src/components/SingleLineEditable.tsx | 99 ++++++++++++++++ packages/ui/src/hooks/useOutsideClick.ts | 49 ++++++++ 14 files changed, 216 insertions(+), 160 deletions(-) delete mode 100644 apps/builder/src/components/SingleLineEditable.tsx rename {apps/builder => packages/ui}/src/components/MultiLineEditable.tsx (51%) create mode 100644 packages/ui/src/components/SingleLineEditable.tsx create mode 100644 packages/ui/src/hooks/useOutsideClick.ts diff --git a/apps/builder/src/components/SingleLineEditable.tsx b/apps/builder/src/components/SingleLineEditable.tsx deleted file mode 100644 index 5475e7986..000000000 --- a/apps/builder/src/components/SingleLineEditable.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Input, type InputProps } from "@typebot.io/ui/components/Input"; -import { cn } from "@typebot.io/ui/lib/cn"; -import { - type ButtonHTMLAttributes, - forwardRef, - type HTMLAttributes, - useRef, - useState, -} from "react"; -import { useOutsideClick } from "@/hooks/useOutsideClick"; - -export type SingleLineEditableProps = { - className?: string; - input?: Omit; - preview?: ButtonHTMLAttributes; - common?: HTMLAttributes; - defaultEdit?: boolean; - value?: string; - defaultValue?: string; - onValueCommit: (value: string) => void; - onClick?: (e: React.MouseEvent) => void; - children?: React.ReactNode; -}; - -export const SingleLineEditable = forwardRef< - HTMLDivElement, - SingleLineEditableProps ->( - ( - { - input, - preview, - common, - value, - defaultValue, - defaultEdit, - onValueCommit, - children, - ...props - }, - ref, - ) => { - const inputRef = useRef(null); - const [isEditing, setIsEditing] = useState(defaultEdit ?? false); - const [currentValue, setCurrentValue] = useState(defaultValue ?? ""); - - const commitValue = () => { - setIsEditing(false); - onValueCommit(value ?? currentValue); - }; - - useOutsideClick({ - ref: inputRef, - capture: true, - handler: commitValue, - }); - - return ( -
- {isEditing ? ( - { - input?.onKeyDownCapture?.(e); - if (e.key === "Enter") commitValue(); - if (e.key === "Escape") setIsEditing(false); - }} - size="none" - value={value ?? currentValue} - autoFocus - onFocus={(e) => e.currentTarget.select()} - onValueChange={(value, e) => { - setCurrentValue(value); - input?.onValueChange?.(value, e); - }} - className={cn( - "p-1 rounded-md border", - common?.className, - input?.className, - )} - /> - ) : ( - - )} - {children} -
- ); - }, -); -SingleLineEditable.displayName = "SingleLineEditable"; diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx index 8ca4c3461..7008784be 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx +++ b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx @@ -8,10 +8,10 @@ import { convertStrToList } from "@typebot.io/lib/convertStrToList"; import { isEmpty } from "@typebot.io/lib/utils"; import { Button } from "@typebot.io/ui/components/Button"; import { Popover } from "@typebot.io/ui/components/Popover"; +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { Settings01Icon } from "@typebot.io/ui/icons/Settings01Icon"; import { cx } from "@typebot.io/ui/lib/cva"; import { useState } from "react"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; import { useTypebot } from "@/features/editor/providers/TypebotProvider"; import { useGraph } from "@/features/graph/providers/GraphProvider"; import { ButtonsItemSettings } from "./ButtonsItemSettings"; diff --git a/apps/builder/src/features/blocks/inputs/cards/components/CardsItemNode.tsx b/apps/builder/src/features/blocks/inputs/cards/components/CardsItemNode.tsx index 223ef76a6..225abde64 100644 --- a/apps/builder/src/features/blocks/inputs/cards/components/CardsItemNode.tsx +++ b/apps/builder/src/features/blocks/inputs/cards/components/CardsItemNode.tsx @@ -6,21 +6,21 @@ import type { } from "@typebot.io/blocks-core/schemas/items/schema"; import type { CardsItem } from "@typebot.io/blocks-inputs/cards/schema"; import { Button } from "@typebot.io/ui/components/Button"; +import { + MultiLineEditable, + type MultiLineEditableProps, +} from "@typebot.io/ui/components/MultiLineEditable"; import { Popover } from "@typebot.io/ui/components/Popover"; +import { + SingleLineEditable, + type SingleLineEditableProps, +} from "@typebot.io/ui/components/SingleLineEditable"; import { Settings01Icon } from "@typebot.io/ui/icons/Settings01Icon"; import { cn } from "@typebot.io/ui/lib/cn"; import { cx } from "@typebot.io/ui/lib/cva"; import { useState } from "react"; import { ImageOrPlaceholder } from "@/components/ImageOrPlaceholder"; import { ImageUploadContent } from "@/components/ImageUploadContent/ImageUploadContent"; -import { - MultiLineEditable, - type MultiLineEditableProps, -} from "@/components/MultiLineEditable"; -import { - SingleLineEditable, - type SingleLineEditableProps, -} from "@/components/SingleLineEditable"; import { GhostableItem, StacksWithGhostableItems, diff --git a/apps/builder/src/features/editor/components/EditableTypebotName.tsx b/apps/builder/src/features/editor/components/EditableTypebotName.tsx index 3ffe2b30b..edbfe0eec 100644 --- a/apps/builder/src/features/editor/components/EditableTypebotName.tsx +++ b/apps/builder/src/features/editor/components/EditableTypebotName.tsx @@ -1,7 +1,7 @@ import { useTranslate } from "@tolgee/react"; +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { Tooltip } from "@typebot.io/ui/components/Tooltip"; import { useState } from "react"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; type EditableProps = { defaultName: string; diff --git a/apps/builder/src/features/folders/components/FolderButton.tsx b/apps/builder/src/features/folders/components/FolderButton.tsx index a28fb464c..b6e6b5171 100644 --- a/apps/builder/src/features/folders/components/FolderButton.tsx +++ b/apps/builder/src/features/folders/components/FolderButton.tsx @@ -5,6 +5,7 @@ import { Alert } from "@typebot.io/ui/components/Alert"; import { AlertDialog } from "@typebot.io/ui/components/AlertDialog"; import { Button, buttonVariants } from "@typebot.io/ui/components/Button"; import { Menu } from "@typebot.io/ui/components/Menu"; +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { Skeleton } from "@typebot.io/ui/components/Skeleton"; import { useOpenControls } from "@typebot.io/ui/hooks/useOpenControls"; import { Folder01SolidIcon } from "@typebot.io/ui/icons/Folder01SolidIcon"; @@ -13,7 +14,6 @@ import { TriangleAlertIcon } from "@typebot.io/ui/icons/TriangleAlertIcon"; import { cn } from "@typebot.io/ui/lib/cn"; import { useRouter } from "next/router"; import { memo, useMemo, useRef, useState } from "react"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; import { orpc } from "@/lib/queryClient"; import { useTypebotDnd } from "../TypebotDndProvider"; diff --git a/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx b/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx index 6f724bf2e..294d98cd3 100644 --- a/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx +++ b/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx @@ -1,11 +1,11 @@ import type { GroupV6 } from "@typebot.io/groups/schemas"; import { isEmpty, isNotDefined } from "@typebot.io/lib/utils"; import { ContextMenu } from "@typebot.io/ui/components/ContextMenu"; +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { cx } from "@typebot.io/ui/lib/cva"; import { useDrag } from "@use-gesture/react"; import { useEffect, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; import { useEditor } from "@/features/editor/providers/EditorProvider"; import { useTypebot } from "@/features/editor/providers/TypebotProvider"; import { groupWidth } from "@/features/graph/constants"; diff --git a/apps/builder/src/features/preview/components/VariablesDrawer.tsx b/apps/builder/src/features/preview/components/VariablesDrawer.tsx index b951f6188..01c446ddd 100644 --- a/apps/builder/src/features/preview/components/VariablesDrawer.tsx +++ b/apps/builder/src/features/preview/components/VariablesDrawer.tsx @@ -10,6 +10,7 @@ import { Field } from "@typebot.io/ui/components/Field"; import { Input } from "@typebot.io/ui/components/Input"; import { MoreInfoTooltip } from "@typebot.io/ui/components/MoreInfoTooltip"; import { Popover } from "@typebot.io/ui/components/Popover"; +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { Switch } from "@typebot.io/ui/components/Switch"; import { useOpenControls } from "@typebot.io/ui/hooks/useOpenControls"; import { Cancel01Icon } from "@typebot.io/ui/icons/Cancel01Icon"; @@ -19,7 +20,6 @@ import { TrashIcon } from "@typebot.io/ui/icons/TrashIcon"; import type { Variable } from "@typebot.io/variables/schemas"; import { useDrag } from "@use-gesture/react"; import { type FormEvent, useState } from "react"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; import { toast } from "@/lib/toast"; import { headerHeight } from "../../editor/constants"; import { useTypebot } from "../../editor/providers/TypebotProvider"; diff --git a/apps/builder/src/features/publish/components/EditableUrl.tsx b/apps/builder/src/features/publish/components/EditableUrl.tsx index 4332f6ddb..ce850252a 100644 --- a/apps/builder/src/features/publish/components/EditableUrl.tsx +++ b/apps/builder/src/features/publish/components/EditableUrl.tsx @@ -1,6 +1,6 @@ +import { SingleLineEditable } from "@typebot.io/ui/components/SingleLineEditable"; import { useState } from "react"; import { CopyButton } from "@/components/CopyButton"; -import { SingleLineEditable } from "@/components/SingleLineEditable"; type EditableUrlProps = { hostname: string; diff --git a/apps/builder/src/features/theme/api/handleSaveThemeTemplate.ts b/apps/builder/src/features/theme/api/handleSaveThemeTemplate.ts index 5ff4caab8..ce1d0839a 100644 --- a/apps/builder/src/features/theme/api/handleSaveThemeTemplate.ts +++ b/apps/builder/src/features/theme/api/handleSaveThemeTemplate.ts @@ -44,17 +44,19 @@ export const handleSaveThemeTemplate = async ({ }, }); - const themeTemplate = (existingThemeTemplate - ? await prisma.themeTemplate.update({ - where: { id: themeTemplateId }, - data, - }) - : await prisma.themeTemplate.create({ - data: { - ...data, - workspaceId, - }, - })) as ThemeTemplate; + const themeTemplate = ( + existingThemeTemplate + ? await prisma.themeTemplate.update({ + where: { id: themeTemplateId }, + data, + }) + : await prisma.themeTemplate.create({ + data: { + ...data, + workspaceId, + }, + }) + ) as ThemeTemplate; return { themeTemplate, diff --git a/bun.lock b/bun.lock index ca6fb0d5c..abd36de07 100644 --- a/bun.lock +++ b/bun.lock @@ -602,7 +602,7 @@ }, "packages/embeds/js": { "name": "@typebot.io/js", - "version": "0.9.22", + "version": "0.9.23", "devDependencies": { "@ai-sdk/ui-utils": "^1.2.11", "@ark-ui/solid": "^5.19.0", @@ -637,7 +637,7 @@ }, "packages/embeds/react": { "name": "@typebot.io/react", - "version": "0.9.22", + "version": "0.9.23", "dependencies": { "@typebot.io/js": "workspace:*", "react": "^19.2.4", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 9da0146f0..00de65f59 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -37,7 +37,10 @@ ], "bundle": true, "thirdParty": true, - "external": ["react", "react/jsx-runtime"], + "external": [ + "react", + "react/jsx-runtime" + ], "declaration": true }, "configurations": { diff --git a/apps/builder/src/components/MultiLineEditable.tsx b/packages/ui/src/components/MultiLineEditable.tsx similarity index 51% rename from apps/builder/src/components/MultiLineEditable.tsx rename to packages/ui/src/components/MultiLineEditable.tsx index 4b22b1f9f..5e4830fcd 100644 --- a/apps/builder/src/components/MultiLineEditable.tsx +++ b/packages/ui/src/components/MultiLineEditable.tsx @@ -1,12 +1,18 @@ -import { Textarea } from "@typebot.io/ui/components/Textarea"; -import { cn } from "@typebot.io/ui/lib/cn"; -import { type ButtonHTMLAttributes, useEffect, useRef, useState } from "react"; -import { useOutsideClick } from "@/hooks/useOutsideClick"; +import { + type ButtonHTMLAttributes, + type TextareaHTMLAttributes, + useEffect, + useRef, + useState, +} from "react"; +import { useOutsideClick } from "../hooks/useOutsideClick"; +import { cn } from "../lib/cn"; +import { Textarea } from "./Textarea"; export type MultiLineEditableProps = { className?: string; input?: Omit< - React.TextareaHTMLAttributes, + TextareaHTMLAttributes, "value" | "onChange" > & { onValueChange?: (value: string) => void; @@ -17,7 +23,7 @@ export type MultiLineEditableProps = { onValueCommit: (value: string) => void; }; -const ADDITIONAL_FOCUS_HEIGHT = 20 as const; +const additionalFocusHeight = 20; export const MultiLineEditable = ({ input, @@ -28,23 +34,26 @@ export const MultiLineEditable = ({ ...props }: MultiLineEditableProps) => { const textareaRef = useRef(null); - const autoResize = (textarea: HTMLTextAreaElement) => { - if (!textarea) return; - textarea.style.height = "auto"; - textarea.style.height = `${textarea.scrollHeight + ADDITIONAL_FOCUS_HEIGHT}px`; - }; - const handleMouseWheel = (e: WheelEvent) => { - e.stopPropagation(); + const autoResize = (textareaElement: HTMLTextAreaElement) => { + textareaElement.style.height = "auto"; + textareaElement.style.height = `${textareaElement.scrollHeight + additionalFocusHeight}px`; }; useEffect(() => { - textareaRef.current?.addEventListener("wheel", handleMouseWheel); + const textareaElement = textareaRef.current; + if (!textareaElement) return; - return () => { - textareaRef.current?.addEventListener("wheel", handleMouseWheel); + const stopMouseWheelPropagation = (event: WheelEvent) => { + event.stopPropagation(); }; - }); + + textareaElement.addEventListener("wheel", stopMouseWheelPropagation); + return () => { + textareaElement.removeEventListener("wheel", stopMouseWheelPropagation); + }; + }, []); + const [isEditing, setIsEditing] = useState(defaultEdit); const commitValue = () => { @@ -63,18 +72,18 @@ export const MultiLineEditable = ({ {isEditing ? (