diff --git a/web/src/beta/components/Icon/Icons/storyPage.svg b/web/src/beta/components/Icon/Icons/storyPage.svg new file mode 100644 index 0000000000..d4ca44839f --- /dev/null +++ b/web/src/beta/components/Icon/Icons/storyPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 0b1b747eea..ecaa0f000b 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -61,6 +61,7 @@ import WorkspaceAdd from "./Icons/workspaceAdd.svg"; import Workspaces from "./Icons/workspaces.svg"; // Storytelling tab +import StoryPage from "./Icons/storyPage.svg"; import Square from "./Icons/square.svg"; import Swiper from "./Icons/swiper.svg"; import Book from "./Icons/book.svg"; @@ -128,6 +129,7 @@ export default { ellipse: Ellipse, playRight: PlayRight, playLeft: PlayLeft, + storyPage: StoryPage, square: Square, swiper: Swiper, book: Book, diff --git a/web/src/beta/components/Icon/index.tsx b/web/src/beta/components/Icon/index.tsx index d42373d39f..b61719213a 100644 --- a/web/src/beta/components/Icon/index.tsx +++ b/web/src/beta/components/Icon/index.tsx @@ -1,5 +1,5 @@ import svgToMiniDataURI from "mini-svg-data-uri"; -import React, { AriaAttributes, AriaRole, CSSProperties, memo, useMemo } from "react"; +import React, { AriaAttributes, AriaRole, CSSProperties, MouseEvent, memo, useMemo } from "react"; import SVG from "react-inlinesvg"; import { ariaProps } from "@reearth/beta/utils/aria"; @@ -20,7 +20,7 @@ export type Props = { role?: AriaRole; notransition?: boolean; transitionDuration?: string; - onClick?: () => void; + onClick?: (e?: MouseEvent) => void; } & AriaAttributes; const Icon: React.FC = ({ diff --git a/web/src/beta/components/fields/ColorField/index.tsx b/web/src/beta/components/fields/ColorField/index.tsx index e1724cc861..b65c9f844f 100644 --- a/web/src/beta/components/fields/ColorField/index.tsx +++ b/web/src/beta/components/fields/ColorField/index.tsx @@ -13,11 +13,9 @@ import Property from ".."; import useHooks from "./hooks"; import { Props, RGBA } from "./types"; -// Constants const channels = ["r", "g", "b", "a"]; const hexPlaceholder = "#RRGGBBAA"; -// Component const ColorField: React.FC = ({ name, description, value, onChange }) => { const t = useT(); const theme = useTheme(); @@ -60,14 +58,14 @@ const ColorField: React.FC = ({ name, description, value, onChange }) => - Color Picker + {t("Color Picker")} {handleClose && } - RGBA + RGBA {channels.map(channel => ( = ({ name, description, value, onChange }) => ); }; -// Styled Components const Wrapper = styled.div` text-align: center; width: 100%; diff --git a/web/src/beta/features/Editor/StoryPanel/ActionPanel/index.tsx b/web/src/beta/features/Editor/StoryPanel/ActionPanel/index.tsx new file mode 100644 index 0000000000..9a068ebbdd --- /dev/null +++ b/web/src/beta/features/Editor/StoryPanel/ActionPanel/index.tsx @@ -0,0 +1,206 @@ +import { Dispatch, Fragment, MouseEvent, SetStateAction, useMemo } from "react"; + +import FieldComponents from "@reearth/beta/components/fields/PropertyFields"; +import Icon, { Icons } from "@reearth/beta/components/Icon"; +import * as Popover from "@reearth/beta/components/Popover"; +import PopoverMenuContent from "@reearth/beta/components/PopoverMenuContent"; +import Text from "@reearth/beta/components/Text"; +import { stopClickPropagation } from "@reearth/beta/utils/events"; +import { Item } from "@reearth/services/api/propertyApi/utils"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +export type ActionItem = { + icon: string; + name?: string; + hide?: boolean; + onClick?: (e: MouseEvent) => void; +}; + +export type ActionPosition = "left-top" | "left-bottom" | "right-top" | "right-bottom"; + +type Props = { + isSelected?: boolean; + showSettings?: boolean; + showPadding?: boolean; + propertyId?: string; + panelSettings?: Item; + actionItems: ActionItem[]; + dndEnabled?: boolean; + position?: ActionPosition; + setShowPadding: Dispatch>; + onSettingsToggle?: () => void; + onRemove?: () => void; +}; + +const ActionPanel: React.FC = ({ + isSelected, + showSettings, + showPadding, + propertyId, + panelSettings, + actionItems, + dndEnabled, + position, + setShowPadding, + onSettingsToggle, + onRemove, +}) => { + const t = useT(); + + const popoverContent = useMemo(() => { + const menuItems: { name: string; icon: Icons; onClick: () => void }[] = [ + { + name: t("Padding settings"), + icon: "padding", + onClick: () => setShowPadding(true), + }, + ]; + if (onRemove) { + menuItems.push({ + name: t("Remove"), + icon: "trash", + onClick: onRemove, + }); + } + return menuItems; + }, [t, setShowPadding, onRemove]); + + return ( + + {dndEnabled && } + onSettingsToggle?.()} + placement="bottom-start"> + + {actionItems.map( + (a, idx) => + !a.hide && ( + + + + + {a.name && ( + + {a.name} + + )} + + + + ), + )} + + + {showPadding ? ( + + + + {panelSettings?.title} + + setShowPadding(false)} /> + + {propertyId && panelSettings && ( + + + + )} + + ) : ( + + )} + + + + ); +}; + +export default ActionPanel; + +const Wrapper = styled.div<{ isSelected?: boolean; position?: ActionPosition }>` + color: ${({ theme }) => theme.select.main}; + display: flex; + align-items: center; + gap: 4px; + height: 24px; + position: absolute; + ${({ position }) => + position === "left-top" + ? ` + left: -1px; + top: -25px; + ` + : position === "left-bottom" + ? ` + left: -1px; + top: 0; + ` + : position === "right-bottom" + ? ` + top: 0; + right: -1px; + ` + : ` + right: -1px; + top: -25px; + `} + transition: all 0.2s; +`; + +const BlockOptions = styled.div<{ isSelected?: boolean }>` + background: ${({ isSelected, theme }) => (isSelected ? theme.select.main : "transparent")}; + color: ${({ isSelected, theme }) => (isSelected ? theme.content.main : theme.select.main)}; + display: flex; + align-items: center; + height: 24px; + transition: all 0.2s; +`; + +const OptionWrapper = styled.div<{ showPointer?: boolean }>` + display: flex; + align-items: center; + cursor: ${({ showPointer }) => (showPointer ? "pointer" : "default")}; +`; + +const OptionText = styled(Text)` + padding-right: 4px; +`; + +const OptionIcon = styled(Icon)<{ border?: boolean }>` + padding: 4px; + ${({ border }) => border && "border-left: 1px solid #f1f1f1;"} +`; + +const SettingsDropdown = styled.div` + z-index: 999; + background: ${({ theme }) => theme.bg[1]}; + border-radius: 2px; + border: 1px solid ${({ theme }) => theme.bg[3]}; +`; + +const SettingsHeading = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; + height: 28px; + padding: 0 8px; +`; + +const SettingsContent = styled.div` + height: 100px; + width: 200px; + padding: 8px; + box-sizing: border-box; +`; + +const CancelIcon = styled(Icon)` + cursor: pointer; +`; + +const DndHandle = styled(Icon)` + cursor: move; +`; diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/Image/index.tsx b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Image/index.tsx index 8d3148e016..d035bfa83d 100644 --- a/web/src/beta/features/Editor/StoryPanel/Block/builtin/Image/index.tsx +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Image/index.tsx @@ -3,9 +3,9 @@ import { useMemo } from "react"; import { ValueTypes } from "@reearth/beta/utils/value"; import { styled } from "@reearth/services/theme"; +import { getFieldValue } from "../../../utils"; import { CommonProps as BlockProps } from "../../types"; import BlockWrapper from "../common/Wrapper"; -import { getFieldValue } from "../utils"; const ImageBlock: React.FC = ({ block, isSelected, ...props }) => { const src = useMemo( diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/Text/index.tsx b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Text/index.tsx index a6f8936fc4..50b3ef25f2 100644 --- a/web/src/beta/features/Editor/StoryPanel/Block/builtin/Text/index.tsx +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Text/index.tsx @@ -3,9 +3,9 @@ import { useMemo } from "react"; import Text from "@reearth/beta/components/Text"; import { ValueTypes } from "@reearth/beta/utils/value"; +import { getFieldValue } from "../../../utils"; import { CommonProps as BlockProps } from "../../types"; import BlockWrapper from "../common/Wrapper"; -import { getFieldValue } from "../utils"; export type Props = BlockProps; diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/Title/index.tsx b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Title/index.tsx new file mode 100644 index 0000000000..9c26f2a7cd --- /dev/null +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/Title/index.tsx @@ -0,0 +1,46 @@ +import { useMemo } from "react"; + +import Text from "@reearth/beta/components/Text"; +import { ValueTypes } from "@reearth/beta/utils/value"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import { getFieldValue } from "../../../utils"; +import { CommonProps as BlockProps } from "../../types"; +import BlockWrapper from "../common/Wrapper"; + +export type Props = BlockProps; + +const TitleBlock: React.FC = ({ block, isSelected, ...props }) => { + const t = useT(); + const text = useMemo( + () => getFieldValue(block?.property?.items ?? [], "title") as ValueTypes["string"], + [block?.property?.items], + ); + + const color = useMemo( + () => getFieldValue(block?.property?.items ?? [], "color") as ValueTypes["string"], + [block?.property?.items], + ); + + return ( + + + {text ?? t("Untitled")} + + + ); +}; + +export default TitleBlock; + +const Title = styled(Text)<{ hasText?: boolean; color?: string }>` + color: ${({ color, hasText, theme }) => (hasText ? color ?? "black" : theme.content.weak)}; +`; diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts index ff03a9e0f0..b52e38d9b1 100644 --- a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts @@ -1,12 +1,14 @@ -import { useMemo } from "react"; +import { MouseEvent, useMemo } from "react"; + +import { ActionItem } from "../../../../ActionPanel"; type Props = { title?: string; icon?: string; isSelected?: boolean; - editMode: boolean; - onEditModeToggle: () => void; - onSettingsToggle: () => void; + editMode?: boolean; + onEditModeToggle?: (e: MouseEvent) => void; + onSettingsToggle?: (e: MouseEvent) => void; }; export default ({ @@ -17,25 +19,32 @@ export default ({ onEditModeToggle, onSettingsToggle, }: Props) => { - const actionItems = useMemo( - () => [ + const actionItems: ActionItem[] = useMemo(() => { + const menuItems: ActionItem[] = [ { - blockName: title ?? "Story Block", + name: title ?? "Story Block", icon: icon ?? "plugin", }, - { + ]; + + if (onEditModeToggle) { + menuItems.push({ icon: editMode ? "exit" : "storyBlockEdit", hide: !isSelected, onClick: onEditModeToggle, - }, - { + }); + } + + if (onSettingsToggle) { + menuItems.push({ icon: "settings", hide: !isSelected, onClick: onSettingsToggle, - }, - ], - [title, icon, isSelected, editMode, onEditModeToggle, onSettingsToggle], - ); + }); + } + + return menuItems; + }, [title, icon, isSelected, editMode, onEditModeToggle, onSettingsToggle]); return { actionItems, }; diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/index.tsx b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/index.tsx index ae0f4c5536..454df6ea0c 100644 --- a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/index.tsx +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/ActionPanel/index.tsx @@ -1,46 +1,40 @@ -import { Dispatch, Fragment, SetStateAction } from "react"; +import { Dispatch, SetStateAction } from "react"; -import FieldComponents from "@reearth/beta/components/fields/PropertyFields"; -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import PopoverMenuContent from "@reearth/beta/components/PopoverMenuContent"; -import Text from "@reearth/beta/components/Text"; import { Item } from "@reearth/services/api/propertyApi/utils"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; + +import ActionPanel, { type ActionPosition } from "../../../../ActionPanel"; import useHooks from "./hooks"; +export { type ActionPosition }; + type Props = { title?: string; icon?: string; isSelected?: boolean; showSettings?: boolean; showPadding?: boolean; - editMode: boolean; + editMode?: boolean; propertyId?: string; panelSettings?: Item; + dndEnabled?: boolean; + position?: ActionPosition; setShowPadding: Dispatch>; - onEditModeToggle: () => void; - onSettingsToggle: () => void; + onEditModeToggle?: () => void; + onSettingsToggle?: () => void; onRemove?: () => void; }; -const ActionPanel: React.FC = ({ +const BlockActionPanel: React.FC = ({ title, icon, isSelected, - showSettings, - showPadding, editMode, - propertyId, - panelSettings, - setShowPadding, + dndEnabled, onEditModeToggle, onSettingsToggle, - onRemove, + ...actionProps }) => { - const t = useT(); const { actionItems } = useHooks({ title, icon, @@ -51,130 +45,14 @@ const ActionPanel: React.FC = ({ }); return ( - - - - - {actionItems.map( - (a, idx) => - !a.hide && ( - - - - - {a.blockName && ( - - {a.blockName} - - )} - - - - ), - )} - - - {showPadding ? ( - - - - {panelSettings?.title} - - setShowPadding(false)} /> - - {propertyId && panelSettings && ( - - - - )} - - ) : ( - setShowPadding(true), - }, - { - name: t("Remove"), - icon: "trash", - onClick: onRemove, - }, - ]} - /> - )} - - - + ); }; -export default ActionPanel; - -const Wrapper = styled.div<{ isSelected?: boolean }>` - color: ${({ theme }) => theme.select.main}; - display: flex; - align-items: center; - gap: 4px; - height: 24px; - position: absolute; - right: -1px; - top: -25px; - transition: all 0.2s; -`; - -const BlockOptions = styled.div<{ isSelected?: boolean }>` - background: ${({ isSelected, theme }) => (isSelected ? theme.select.main : "transparent")}; - color: ${({ isSelected, theme }) => (isSelected ? theme.content.main : theme.select.main)}; - display: flex; - align-items: center; - height: 24px; - transition: all 0.2s; -`; - -const OptionWrapper = styled.div<{ showPointer?: boolean }>` - display: flex; - align-items: center; - ${({ showPointer }) => showPointer && "cursor: pointer;"} -`; - -const OptionText = styled(Text)` - padding-right: 4px; -`; - -const OptionIcon = styled(Icon)` - padding: 4px; - border-left: 1px solid #f1f1f1; -`; - -const SettingsDropdown = styled.div` - z-index: 999; - background: ${({ theme }) => theme.bg[1]}; - border-radius: 2px; - border: 1px solid ${({ theme }) => theme.bg[3]}; -`; - -const SettingsHeading = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; - height: 28px; - padding: 0 8px; -`; - -const SettingsContent = styled.div` - height: 100px; - width: 200px; - padding: 8px; - box-sizing: border-box; -`; - -const CancelIcon = styled(Icon)` - cursor: pointer; -`; +export default BlockActionPanel; diff --git a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/Wrapper.tsx b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/Wrapper.tsx index f78da5dbf6..6589a2f304 100644 --- a/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/Wrapper.tsx +++ b/web/src/beta/features/Editor/StoryPanel/Block/builtin/common/Wrapper.tsx @@ -1,13 +1,13 @@ import { ReactNode } from "react"; import FieldComponents from "@reearth/beta/components/fields/PropertyFields"; +import { stopClickPropagation } from "@reearth/beta/utils/events"; import { type Item } from "@reearth/services/api/propertyApi/utils"; import { styled } from "@reearth/services/theme"; +import SelectableArea from "../../../SelectableArea"; import Template from "../../Template"; -import ActionPanel from "./ActionPanel"; -import ClickAwayListener from "./click-away"; import useHooks from "./hooks"; type Spacing = { @@ -24,6 +24,7 @@ type Props = { children?: ReactNode; propertyId?: string; propertyItems?: Item[]; + dndEnabled?: boolean; onClick?: () => void; onClickAway?: () => void; onRemove?: () => void; @@ -36,24 +37,20 @@ const BlockWrapper: React.FC = ({ children, propertyId, propertyItems, + dndEnabled = true, onClick, onClickAway, onRemove, }) => { const { - isHovered, editMode, showSettings, - showPadding, defaultSettings, - panelSettings, padding, - setShowPadding, - handleMouseEnter, - handleMouseLeave, - handleBlockClick, + setEditMode, handleEditModeToggle, handleSettingsToggle, + handleBlockClick, } = useHooks({ isSelected, propertyItems, @@ -61,55 +58,34 @@ const BlockWrapper: React.FC = ({ }); return ( - - - {(isHovered || isSelected) && ( - - )} - - {children ??