From 6e0470fba9e24b91ca65105e14c3e32f63683d74 Mon Sep 17 00:00:00 2001 From: KaWaite <34051327+KaWaite@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:07:54 +0900 Subject: [PATCH] chore(web): infobox support in beta (#880) --- .github/CODEOWNERS | 4 +- server/pkg/builtin/manifest.yml | 2 +- web/package.json | 8 +- .../beta/components/DragAndDropList/index.tsx | 3 +- .../Icon/Icons/imageInfoboxBlock.svg | 3 + .../components/Icon/Icons/infoboxIcon.svg | 10 +- .../Icon/Icons/propertyInfoboxBlock.svg | 3 + .../{textStoryBlock.svg => textBlock.svg} | 0 web/src/beta/components/Icon/icons.ts | 25 +- web/src/beta/components/ListItem/index.tsx | 1 - .../beta/components/TabMenu/index.stories.tsx | 2 +- web/src/beta/components/TabMenu/index.tsx | 14 +- .../components/fields/SelectField/index.tsx | 2 +- .../components/fields/TextField/index.tsx | 18 +- .../features/Editor/BottomPanel/index.tsx | 1 + .../beta/features/Editor/SidePanel/index.tsx | 20 +- .../Editor/Visualizer/convert-infobox.ts | 27 ++ .../Editor/Visualizer/convert-story.ts | 159 +-------- .../features/Editor/Visualizer/convert.ts | 28 +- .../beta/features/Editor/Visualizer/hooks.ts | 143 ++++---- .../beta/features/Editor/Visualizer/index.tsx | 72 ++-- .../Editor/Visualizer/processNewProperty.ts | 156 +++++++++ web/src/beta/features/Editor/index.tsx | 10 +- .../RightPanel/LayerInspector/FeatureData.tsx | 7 +- .../map/RightPanel/LayerInspector/index.tsx | 17 +- .../LayerInspector/infobox/hooks.ts | 26 ++ .../LayerInspector/infobox/index.tsx | 46 +++ .../Editor/tabs/map/RightPanel/index.tsx | 1 + .../Editor/tabs/map/Toolbar/index.tsx | 134 ++++---- .../Editor/tabs/story/LeftPanel/index.tsx | 1 + .../Editor/tabs/story/RightPanel/index.tsx | 1 + .../Crust/Infobox/AdditionButton/index.tsx | 108 ------ .../Block/builtin/DataList/index.stories.tsx | 56 --- .../Infobox/Block/builtin/DataList/index.tsx | 123 ------- .../Block/builtin/HTML/index.stories.tsx | 33 -- .../Infobox/Block/builtin/HTML/index.tsx | 262 -------------- .../Block/builtin/Image/index.stories.tsx | 98 ------ .../Infobox/Block/builtin/Image/index.tsx | 143 ++------ .../Infobox/Block/builtin/Location/icon.svg | 5 - .../Block/builtin/Location/index.stories.tsx | 33 -- .../Infobox/Block/builtin/Location/index.tsx | 115 ------- .../Block/builtin/PropertyList/Content.tsx | 146 ++++++++ .../PropertyList/CustomFields/index.tsx | 29 ++ .../CustomFields/useEvaluateProperties.ts | 66 ++++ .../builtin/PropertyList/ListEditor/Item.tsx | 78 +++++ .../builtin/PropertyList/ListEditor/hooks.ts | 146 ++++++++ .../builtin/PropertyList/ListEditor/index.tsx | 136 ++++++++ .../Block/builtin/PropertyList/ListItem.tsx | 48 +++ .../Block/builtin/PropertyList/index.tsx | 25 ++ .../Block/builtin/Text/index.stories.tsx | 65 ---- .../Infobox/Block/builtin/Text/index.tsx | 238 ++----------- .../Block/builtin/Video/index.stories.tsx | 36 -- .../Infobox/Block/builtin/Video/index.tsx | 114 ------ .../core/Crust/Infobox/Block/builtin/index.ts | 71 ++-- .../Infobox/Block/builtin/unsafeBlocks.ts | 33 -- .../Block/builtin/useExpressionEval.ts | 54 +++ .../Crust/Infobox/Block/index.stories.tsx | 21 -- .../lib/core/Crust/Infobox/Block/index.tsx | 67 ++-- .../lib/core/Crust/Infobox/Block/utils.ts | 31 -- .../lib/core/Crust/Infobox/Field/hooks.ts | 86 ----- .../Crust/Infobox/Field/index.stories.tsx | 28 -- .../lib/core/Crust/Infobox/Field/index.tsx | 130 ------- .../Crust/Infobox/Frame/index.stories.tsx | 30 -- .../lib/core/Crust/Infobox/Frame/index.tsx | 321 ----------------- .../beta/lib/core/Crust/Infobox/constants.ts | 17 + web/src/beta/lib/core/Crust/Infobox/hooks.ts | 146 ++++++-- .../lib/core/Crust/Infobox/index.stories.tsx | 83 ----- .../lib/core/Crust/Infobox/index.test.tsx | 16 - web/src/beta/lib/core/Crust/Infobox/index.tsx | 325 +++++++++--------- web/src/beta/lib/core/Crust/Infobox/types.ts | 51 +-- web/src/beta/lib/core/Crust/Infobox/utils.ts | 1 - .../lib/core/Crust/Plugins/Plugin/hooks.ts | 2 +- .../lib/core/Crust/Plugins/Plugin/index.tsx | 2 +- web/src/beta/lib/core/Crust/Plugins/api.ts | 2 +- .../lib/core/Crust/Plugins/plugin_types.ts | 4 +- .../core/Crust/Plugins/usePluginInstances.ts | 2 +- web/src/beta/lib/core/Crust/hooks.tsx | 15 +- web/src/beta/lib/core/Crust/index.tsx | 142 ++++---- web/src/beta/lib/core/Map/Layers/hooks.ts | 33 +- web/src/beta/lib/core/Map/ref.ts | 1 + .../lib/core/StoryPanel/Block/Template.tsx | 9 +- .../StoryPanel/Block/builtin/Camera/index.tsx | 5 +- .../StoryPanel/Block/builtin/Image/index.tsx | 6 +- .../Block/builtin/Layer/Content.tsx | 10 +- .../StoryPanel/Block/builtin/Layer/index.tsx | 5 +- .../Block/builtin/Markdown/index.tsx | 5 +- .../Block/builtin/NextPage/index.tsx | 5 +- .../StoryPanel/Block/builtin/Text/index.tsx | 5 +- .../StoryPanel/Block/builtin/Timeline/hook.ts | 7 +- .../Block/builtin/Timeline/index.tsx | 6 +- .../StoryPanel/Block/builtin/Title/index.tsx | 7 +- .../StoryPanel/Block/builtin/Video/index.tsx | 6 +- .../beta/lib/core/StoryPanel/Block/index.tsx | 15 +- .../beta/lib/core/StoryPanel/Page/hooks.ts | 8 +- .../beta/lib/core/StoryPanel/Page/index.tsx | 2 +- .../lib/core/StoryPanel/PanelContent/hooks.ts | 8 +- web/src/beta/lib/core/StoryPanel/context.tsx | 4 - web/src/beta/lib/core/StoryPanel/index.tsx | 112 +++--- web/src/beta/lib/core/Visualizer/hooks.ts | 98 +----- web/src/beta/lib/core/Visualizer/index.tsx | 110 +++--- web/src/beta/lib/core/engines/Cesium/hooks.ts | 6 +- .../lib/core/engines/Cesium/utils/utils.ts | 2 +- .../lib/core/mantle/compat/backward.test.ts | 6 +- .../lib/core/mantle/compat/forward.test.ts | 6 +- web/src/beta/lib/core/mantle/compat/types.ts | 37 +- .../lib/core/mantle/evaluator/simple/index.ts | 10 +- web/src/beta/lib/core/mantle/types/index.ts | 5 +- .../shared/components/BlockAddBar/index.tsx | 5 +- .../shared/components/BlockWrapper/hooks.ts | 36 +- .../shared/components/BlockWrapper/index.tsx | 8 +- .../lib/core/shared/contexts/blockContext.tsx | 22 ++ .../core/shared/contexts/editModeContext.tsx | 22 ++ .../core/shared/hooks/useFieldComponent.tsx | 21 +- .../{StoryPanel/Block => shared}/types.ts | 27 +- web/src/services/api/index.ts | 1 + web/src/services/api/infoboxApi/blocks.ts | 172 +++++++++ web/src/services/api/infoboxApi/index.tsx | 58 ++++ web/src/services/api/layersApi/utils.ts | 20 +- web/src/services/api/projectApi.ts | 3 +- web/src/services/gql/__gen__/gql.ts | 29 +- web/src/services/gql/__gen__/graphql.ts | 50 ++- web/src/services/gql/fragments/layer.ts | 23 +- web/src/services/gql/queries/infobox.ts | 54 +++ web/src/services/i18n/translations/en.yml | 9 +- web/src/services/i18n/translations/ja.yml | 9 +- web/yarn.lock | 12 +- 126 files changed, 2524 insertions(+), 3323 deletions(-) create mode 100644 web/src/beta/components/Icon/Icons/imageInfoboxBlock.svg create mode 100644 web/src/beta/components/Icon/Icons/propertyInfoboxBlock.svg rename web/src/beta/components/Icon/Icons/{textStoryBlock.svg => textBlock.svg} (100%) create mode 100644 web/src/beta/features/Editor/Visualizer/convert-infobox.ts create mode 100644 web/src/beta/features/Editor/Visualizer/processNewProperty.ts create mode 100644 web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/hooks.ts create mode 100644 web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/AdditionButton/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Image/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Location/icon.svg delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Location/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Location/index.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/Content.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/CustomFields/index.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/CustomFields/useEvaluateProperties.ts create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/ListEditor/Item.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/ListEditor/hooks.ts create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/ListEditor/index.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/ListItem.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/PropertyList/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Text/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Video/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/Video/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/unsafeBlocks.ts create mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/builtin/useExpressionEval.ts delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Block/utils.ts delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Field/hooks.ts delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Field/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Field/index.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Frame/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/Frame/index.tsx create mode 100644 web/src/beta/lib/core/Crust/Infobox/constants.ts delete mode 100644 web/src/beta/lib/core/Crust/Infobox/index.stories.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/index.test.tsx delete mode 100644 web/src/beta/lib/core/Crust/Infobox/utils.ts create mode 100644 web/src/beta/lib/core/shared/contexts/blockContext.tsx create mode 100644 web/src/beta/lib/core/shared/contexts/editModeContext.tsx rename web/src/beta/lib/core/{StoryPanel/Block => shared}/types.ts (79%) create mode 100644 web/src/services/api/infoboxApi/blocks.ts create mode 100644 web/src/services/api/infoboxApi/index.tsx create mode 100644 web/src/services/gql/queries/infobox.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d06abcc5f4..529d00660b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,9 +2,9 @@ /server/ @pyshx -/web/e2e/ @keiya01 -/web/src/ @airslice @mkumbobeaty +/web/ @airslice @mkumbobeaty +/web/e2e/ @keiya01 /web/src/classic/ @airslice /web/src/classic/core/ @keiya01 /web/src/beta/lib/core/ @keiya01 diff --git a/server/pkg/builtin/manifest.yml b/server/pkg/builtin/manifest.yml index cf391b3665..c37e6e02a5 100644 --- a/server/pkg/builtin/manifest.yml +++ b/server/pkg/builtin/manifest.yml @@ -2708,4 +2708,4 @@ extensions: availableIf: field: displayType type: string - value: custom \ No newline at end of file + value: custom diff --git a/web/package.json b/web/package.json index 26e7bf1f97..a30a6daf37 100644 --- a/web/package.json +++ b/web/package.json @@ -117,7 +117,7 @@ "@sentry/browser": "7.77.0", "@seznam/compose-react-refs": "1.0.6", "@turf/turf": "6.5.0", - "@types/d3": "^7.4.3", + "@types/d3": "7.4.3", "@types/escape-string-regexp": "2.0.1", "@ungap/event-target": "0.2.4", "@xstate/react": "3.2.1", @@ -132,7 +132,7 @@ "core-js": "3.33.2", "crypto-js": "4.2.0", "csv-parse": "5.5.2", - "d3": "^7.8.5", + "d3": "7.8.5", "date-fns": "2.30.0", "dayjs": "1.11.10", "detect-browser": "5.3.0", @@ -185,7 +185,7 @@ "react-router-dom": "6.21.0", "react-spinners": "0.13.8", "react-use": "17.4.0", - "react18-json-view": "0.2.6", + "react18-json-view": "0.2.7", "remark-gfm": "3.0.1", "resium": "1.16.1", "suspend-react": "^0.1.3", @@ -197,4 +197,4 @@ "uuid": "9.0.1", "xstate": "4.38.2" } -} \ No newline at end of file +} diff --git a/web/src/beta/components/DragAndDropList/index.tsx b/web/src/beta/components/DragAndDropList/index.tsx index 9d842a7f12..7f882cc8e1 100644 --- a/web/src/beta/components/DragAndDropList/index.tsx +++ b/web/src/beta/components/DragAndDropList/index.tsx @@ -1,5 +1,4 @@ -import type { ReactNode } from "react"; -import { useCallback, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; import { useDragDropManager } from "react-dnd"; import { styled } from "@reearth/services/theme"; diff --git a/web/src/beta/components/Icon/Icons/imageInfoboxBlock.svg b/web/src/beta/components/Icon/Icons/imageInfoboxBlock.svg new file mode 100644 index 0000000000..27b39cba65 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/imageInfoboxBlock.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/infoboxIcon.svg b/web/src/beta/components/Icon/Icons/infoboxIcon.svg index ea693133a9..31f188ddcf 100644 --- a/web/src/beta/components/Icon/Icons/infoboxIcon.svg +++ b/web/src/beta/components/Icon/Icons/infoboxIcon.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/web/src/beta/components/Icon/Icons/propertyInfoboxBlock.svg b/web/src/beta/components/Icon/Icons/propertyInfoboxBlock.svg new file mode 100644 index 0000000000..371dd6802c --- /dev/null +++ b/web/src/beta/components/Icon/Icons/propertyInfoboxBlock.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/textStoryBlock.svg b/web/src/beta/components/Icon/Icons/textBlock.svg similarity index 100% rename from web/src/beta/components/Icon/Icons/textStoryBlock.svg rename to web/src/beta/components/Icon/Icons/textBlock.svg diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 5ebda9cdce..e265486cbd 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -4,13 +4,11 @@ // Primitives import PrimPhotoOverlay from "./Icons/primPhotoIcon.svg?react"; -// Infobox Blocks +// Infobox import Infobox from "./Icons/infoboxIcon.svg?react"; -import InfoHTML from "./Icons/infoboxHTMLIcon.svg?react"; -import InfoLocation from "./Icons/infoboxLocationIcon.svg?react"; -import InfoTable from "./Icons/infoboxTableIcon.svg?react"; -import InfoText from "./Icons/infoboxTextIcon.svg?react"; -import InfoVideo from "./Icons/infoboxVideoIcon.svg?react"; +import InfoboxImageBlock from "./Icons/imageInfoboxBlock.svg?react"; +import TextBlock from "./Icons/textBlock.svg?react"; +import InfoboxPropertyBlock from "./Icons/propertyInfoboxBlock.svg?react"; // Arrow import ArrowUpDown from "./Icons/arrowUpDown.svg?react"; @@ -82,7 +80,6 @@ import Copy from "./Icons/copy.svg?react"; import GearSix from "./Icons/gearSix.svg?react"; import PencilSimple from "./Icons/pencilSimple.svg?react"; import Trash from "./Icons/trash.svg?react"; -import TextStoryBlock from "./Icons/textStoryBlock.svg?react"; import Edit from "./Icons/storyBlockEdit.svg?react"; import Exit from "./Icons/exit.svg?react"; import Settings from "./Icons/settings.svg?react"; @@ -143,13 +140,6 @@ export default { addLayer: AddLayerIcon, zoomToLayer: ZoomToLayer, file: File, - dl: InfoTable, - infobox: Infobox, - text: InfoText, - html: InfoHTML, - video: InfoVideo, - clock: Clock, - location: InfoLocation, photooverlay: PrimPhotoOverlay, arrowUpDown: ArrowUpDown, arrowRight: ArrowRight, @@ -199,7 +189,12 @@ export default { exit: Exit, settings: Settings, padding: Padding, - textStoryBlock: TextStoryBlock, + infobox: Infobox, + imageInfoboxBetaBlock: InfoboxImageBlock, + textInfoboxBetaBlock: TextBlock, + propertyInfoboxBetaBlock: InfoboxPropertyBlock, + clock: Clock, + textStoryBlock: TextBlock, titleStoryBlock: TitleStoryBlock, videoStoryBlock: VideoStoryBlock, imageStoryBlock: ImageStoryBlock, diff --git a/web/src/beta/components/ListItem/index.tsx b/web/src/beta/components/ListItem/index.tsx index bee85c524e..643416528b 100644 --- a/web/src/beta/components/ListItem/index.tsx +++ b/web/src/beta/components/ListItem/index.tsx @@ -68,7 +68,6 @@ const Wrapper = styled.div<{ clamp?: Clamp; }>` display: flex; - width: 100%; min-height: 36px; align-items: center; color: ${({ theme }) => theme.content.main}; diff --git a/web/src/beta/components/TabMenu/index.stories.tsx b/web/src/beta/components/TabMenu/index.stories.tsx index 4c2e84f049..a193758766 100644 --- a/web/src/beta/components/TabMenu/index.stories.tsx +++ b/web/src/beta/components/TabMenu/index.stories.tsx @@ -87,7 +87,7 @@ Default.args = { { id: "tab1", name: "My infobox", icon: "infobox", component: }, { id: "tab2", - icon: "dl", + icon: "marker", component: Tab 2. Can be any react component, }, ], diff --git a/web/src/beta/components/TabMenu/index.tsx b/web/src/beta/components/TabMenu/index.tsx index 3d662c40d2..a09385ce3a 100644 --- a/web/src/beta/components/TabMenu/index.tsx +++ b/web/src/beta/components/TabMenu/index.tsx @@ -46,7 +46,7 @@ const TabMenu: FC = ({ tabs, selectedTab, onSelectedTabChange, menuAlignm {selectedTabItem.name} )} - {selectedTabItem ? selectedTabItem.component : null} + {selectedTabItem ? selectedTabItem.component : null} ); @@ -60,14 +60,14 @@ const Wrapper = styled.div<{ menuAlignment?: menuAlignment }>` flex-flow: column nowrap; position: relative; background: ${({ theme }) => theme.bg[1]}; - border-radius: 4px; + height: 100%; `; const Tabs = styled.div<{ menuAlignment?: menuAlignment }>` - padding-top: 4px; - background: ${({ theme }) => theme.bg[0]}; display: flex; flex-flow: ${({ menuAlignment }) => (menuAlignment === "top" ? "row" : "column")} nowrap; + padding-top: 4px; + background: ${({ theme }) => theme.bg[0]}; `; const TabIconWrapper = styled.div<{ selected: boolean; menuAlignment?: menuAlignment }>` @@ -87,12 +87,14 @@ const TabHeader = styled(Text)` `; const Header = styled.div` - padding-bottom: 12px; - margin-bottom: 12px; + padding: 12px; border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; `; const MainArea = styled.div` display: block; +`; + +const Content = styled.div` padding: 12px; `; diff --git a/web/src/beta/components/fields/SelectField/index.tsx b/web/src/beta/components/fields/SelectField/index.tsx index 9acee3b581..691a5b5575 100644 --- a/web/src/beta/components/fields/SelectField/index.tsx +++ b/web/src/beta/components/fields/SelectField/index.tsx @@ -153,7 +153,7 @@ const ProviderWrapper = styled.div<{ multiSelect: boolean }>` flex-direction: column; gap: 6px; border-radius: 4px; - padding: 4px; + width: 100%; `; const InputWrapper = styled.div<{ disabled: boolean }>` diff --git a/web/src/beta/components/fields/TextField/index.tsx b/web/src/beta/components/fields/TextField/index.tsx index 61db1e6b35..2d12f1864c 100644 --- a/web/src/beta/components/fields/TextField/index.tsx +++ b/web/src/beta/components/fields/TextField/index.tsx @@ -3,25 +3,35 @@ import TextInput from "@reearth/beta/components/fields/common/TextInput"; import Property from ".."; export type Props = { + className?: string; name?: string; description?: string; value?: string; placeholder?: string; - onChange?: (text: string) => void; disabled?: boolean; + onChange?: (text: string) => void; + onBlur?: () => void; }; const TextField: React.FC = ({ + className, name, description, value, placeholder, - onChange, disabled, + onChange, + onBlur, }) => { return ( - - + + ); }; diff --git a/web/src/beta/features/Editor/BottomPanel/index.tsx b/web/src/beta/features/Editor/BottomPanel/index.tsx index 5f1f3c2d69..7214fbdcdf 100644 --- a/web/src/beta/features/Editor/BottomPanel/index.tsx +++ b/web/src/beta/features/Editor/BottomPanel/index.tsx @@ -32,6 +32,7 @@ const Wrapper = styled.div` height: 100%; border-left: 1px solid ${({ theme }) => theme.outline.weakest}; border-right: 1px solid ${({ theme }) => theme.outline.weakest}; + background: ${({ theme }) => theme.bg[0]}; `; const TitleWrapper = styled.div` diff --git a/web/src/beta/features/Editor/SidePanel/index.tsx b/web/src/beta/features/Editor/SidePanel/index.tsx index afb1bc5e01..3b761e4872 100644 --- a/web/src/beta/features/Editor/SidePanel/index.tsx +++ b/web/src/beta/features/Editor/SidePanel/index.tsx @@ -15,9 +15,10 @@ export type SidePanelContent = { type Props = { location: "left" | "right"; contents: SidePanelContent[]; + padding?: number; }; -const Panel: React.FC = ({ location, contents }) => { +const Panel: React.FC = ({ location, contents, padding }) => { return ( {contents.map( @@ -27,7 +28,7 @@ const Panel: React.FC = ({ location, contents }) => { {content.title} {content.actions && {content.actions}} - {content.children} + {content.children} ), @@ -43,9 +44,9 @@ const Wrapper = styled.div<{ location: "left" | "right" }>` flex-direction: column; width: 100%; height: 100%; - box-sizing: border-box; gap: 4px; - background: ${({ theme }) => theme.bg[1]}; + box-sizing: border-box; + padding: 2px 1px; `; const Section = styled.div<{ maxHeight?: CSSProperties["maxHeight"] }>` @@ -63,16 +64,19 @@ const Card = styled.div` `; const Title = styled(Text)` - background: ${({ theme }) => theme.bg[1]}; - padding: 8px; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + background: ${({ theme }) => theme.bg[2]}; + padding: 4px 12px; `; const ActionArea = styled.div` padding: 8px; `; -const Content = styled.div<{ hasActions?: boolean }>` - padding: ${({ hasActions }) => (hasActions ? "0" : "8px")}; +const Content = styled.div<{ padding?: number }>` + ${({ padding }) => padding && `padding: ${padding}px;`} + border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; overflow-y: auto; diff --git a/web/src/beta/features/Editor/Visualizer/convert-infobox.ts b/web/src/beta/features/Editor/Visualizer/convert-infobox.ts new file mode 100644 index 0000000000..c4196822fc --- /dev/null +++ b/web/src/beta/features/Editor/Visualizer/convert-infobox.ts @@ -0,0 +1,27 @@ +import { InfoboxBlock } from "@reearth/beta/lib/core/Crust/Infobox/types"; +import type { Layer } from "@reearth/beta/lib/core/Map"; +import { NLSInfobox } from "@reearth/services/api/layersApi/utils"; + +import { processProperty as processNewProperty } from "./processNewProperty"; + +export default ( + orig: NLSInfobox | null | undefined, + parent: NLSInfobox | null | undefined, + blockNames?: { + [key: string]: string; + }, +): Layer["infobox"] => { + const used = orig || parent; + if (!used) return; + return { + property: processNewProperty(parent?.property, orig?.property), + blocks: used.blocks?.map(b => ({ + id: b.id, + name: blockNames?.[b.extensionId] ?? "Infobox Block", + pluginId: b.pluginId, + extensionId: b.extensionId, + property: processNewProperty(undefined, b.property), + propertyId: b.propertyId, // required by onBlockChange + })), + }; +}; diff --git a/web/src/beta/features/Editor/Visualizer/convert-story.ts b/web/src/beta/features/Editor/Visualizer/convert-story.ts index 0493761cba..a8fe60a90f 100644 --- a/web/src/beta/features/Editor/Visualizer/convert-story.ts +++ b/web/src/beta/features/Editor/Visualizer/convert-story.ts @@ -1,19 +1,8 @@ import { Story, StoryBlock, StoryPage } from "@reearth/beta/lib/core/StoryPanel/types"; -import { valueFromGQL, valueTypeFromGQL } from "@reearth/beta/utils/value"; -import { toUi } from "@reearth/services/api/propertyApi/utils"; import { Scene } from "@reearth/services/api/sceneApi"; -import { - PropertyFieldFragmentFragment, - PropertyFragmentFragment, - PropertyGroupFragmentFragment, - PropertyItemFragmentFragment, - PropertySchemaFieldFragmentFragment, - PropertySchemaGroupFragmentFragment, - StoryPage as GqlStoryPage, - StoryBlock as GqlStoryBlock, -} from "@reearth/services/gql"; +import { StoryPage as GqlStoryPage, StoryBlock as GqlStoryBlock } from "@reearth/services/gql"; -import { DatasetMap, P, datasetValue } from "./convert"; +import { processProperty } from "./processNewProperty"; export const convertStory = (scene?: Scene, storyId?: string): Story | undefined => { const story = scene?.stories.find(s => s.id === storyId); @@ -55,147 +44,3 @@ export const convertStory = (scene?: Scene, storyId?: string): Story | undefined pages: storyPages(story.pages), }; }; - -export const processProperty = ( - parent: PropertyFragmentFragment | null | undefined, - orig?: PropertyFragmentFragment | null | undefined, - linkedDatasetId?: string | null | undefined, - datasets?: DatasetMap | null | undefined, -): P | undefined => { - const schema = orig?.schema || parent?.schema; - if (!schema) return; - - const allItems: Record< - string, - { - schema: PropertySchemaGroupFragmentFragment; - orig?: PropertyItemFragmentFragment; - parent?: PropertyItemFragmentFragment; - } - > = schema.groups.reduce( - (a, b) => ({ - ...a, - [b.schemaGroupId]: { - schema: b, - orig: orig?.items.find(i => i.schemaGroupId === b.schemaGroupId), - parent: parent?.items.find(i => i.schemaGroupId === b.schemaGroupId), - }, - }), - {}, - ); - const mergedProperty: P = Object.fromEntries( - Object.entries(allItems) - .map(([key, value]) => { - const { schema, orig, parent } = value; - if (!orig && !parent) { - if (schema.isList) { - return [key, undefined]; - } - return [ - key, - processPropertyGroups(schema, undefined, undefined, linkedDatasetId, datasets), - ]; - } - - if ( - (!orig || orig.__typename === "PropertyGroupList") && - (!parent || parent.__typename === "PropertyGroupList") - ) { - const used = orig || parent; - return [ - key, - used?.groups.map(g => ({ - ...processPropertyGroups(schema, g, undefined, linkedDatasetId, datasets), - id: g.id, - })), - ]; - } - - if ( - (!orig || orig.__typename === "PropertyGroup") && - (!parent || parent.__typename === "PropertyGroup") - ) { - return [key, processPropertyGroups(schema, parent, orig, linkedDatasetId, datasets)]; - } - return [key, null]; - }) - .filter(([, value]) => !!value), - ); - - return mergedProperty; -}; - -const processPropertyGroups = ( - schema: PropertySchemaGroupFragmentFragment, - parent: PropertyGroupFragmentFragment | null | undefined, - original: PropertyGroupFragmentFragment | null | undefined, - linkedDatasetId: string | null | undefined, - datasets: DatasetMap | null | undefined, -): any => { - const allFields: Record< - string, - { - schema: PropertySchemaFieldFragmentFragment; - parent?: PropertyFieldFragmentFragment; - orig?: PropertyFieldFragmentFragment; - } - > = schema.fields.reduce( - (a, b) => ({ - ...a, - [b.fieldId]: { - schema: b, - parent: parent?.fields.find(i => i.fieldId === b.fieldId), - orig: original?.fields.find(i => i.fieldId === b.fieldId), - }, - }), - {}, - ); - - return Object.fromEntries( - Object.entries(allFields).map(([key, { schema, parent, orig }]) => { - const used = orig || parent; - - const fieldMeta = { - type: valueTypeFromGQL(schema.type) || undefined, - ui: toUi(schema.ui) || undefined, - title: schema.translatedTitle || undefined, - description: schema.translatedDescription || undefined, - choices: schema.choices || undefined, - min: schema.min || undefined, - max: schema.max || undefined, - }; - - if (!used) { - return [ - key, - { - ...fieldMeta, - value: schema.defaultValue - ? valueFromGQL(schema.defaultValue, schema.type)?.value - : undefined, - }, - ]; - } - - const datasetSchemaId = used?.links?.[0]?.datasetSchemaId; - const datasetFieldId = used?.links?.[0]?.datasetSchemaFieldId; - if (datasetSchemaId && linkedDatasetId && datasetFieldId) { - return [ - key, - { - ...fieldMeta, - value: datasetValue(datasets, datasetSchemaId, linkedDatasetId, datasetFieldId), - }, - ]; - } - - return [ - key, - { - ...fieldMeta, - value: valueFromGQL(used.value, used.type)?.value, - }, - ]; - }), - ); -}; diff --git a/web/src/beta/features/Editor/Visualizer/convert.ts b/web/src/beta/features/Editor/Visualizer/convert.ts index bff1871785..82744c68ed 100644 --- a/web/src/beta/features/Editor/Visualizer/convert.ts +++ b/web/src/beta/features/Editor/Visualizer/convert.ts @@ -33,6 +33,8 @@ import { NlsLayerCommonFragment, } from "@reearth/services/gql"; +import convertInfobox from "./convert-infobox"; + export type P = { [key in string]: any }; export type DatasetMap = Record; @@ -344,7 +346,10 @@ export type RawNLSLayer = NlsLayerCommonFragment & { export function processLayers( newLayers?: NLSLayer[], layerStyles?: LayerStyle[], - _parent?: RawNLSLayer | null | undefined, + parent?: RawNLSLayer | null | undefined, + infoboxBlockNames?: { + [key: string]: string; + }, ): Layer[] | undefined { const getLayerStyleValue = (id?: string) => { const layerStyleValue: Partial = layerStyles?.find( @@ -368,8 +373,7 @@ export function processLayers( id: nlsLayer.id, title: nlsLayer.title, visible: nlsLayer.visible, - infobox: undefined, - // infobox: nlsLayer.infobox ? processInfobox(nlsLayer.infobox, parent?.infobox) : undefined, + infobox: convertInfobox(nlsLayer.infobox, parent?.infobox, infoboxBlockNames), properties: nlsLayer.config?.properties, defines: nlsLayer.config?.defines, events: nlsLayer.config?.events, @@ -378,21 +382,3 @@ export function processLayers( }; }); } - -// const processInfobox = ( -// orig: EarthLayerFragment["infobox"] | null | undefined, -// parent: EarthLayerFragment["infobox"] | null | undefined, -// ): Layer["infobox"] => { -// const used = orig || parent; -// if (!used) return; -// return { -// property: processProperty(parent?.property, orig?.property), -// blocks: used.fields.map(f => ({ -// id: f.id, -// pluginId: f.pluginId, -// extensionId: f.extensionId, -// property: processProperty(undefined, f.property), -// propertyId: f.propertyId, // required by onBlockChange -// })), -// }; -// }; diff --git a/web/src/beta/features/Editor/Visualizer/hooks.ts b/web/src/beta/features/Editor/Visualizer/hooks.ts index 92ea6921e1..fb96805e27 100644 --- a/web/src/beta/features/Editor/Visualizer/hooks.ts +++ b/web/src/beta/features/Editor/Visualizer/hooks.ts @@ -1,13 +1,8 @@ import { useMemo, useEffect, useCallback } from "react"; import type { Alignment, Location } from "@reearth/beta/lib/core/Crust"; -import type { - LatLng, - ValueTypes, - ComputedLayer, - ComputedFeature, -} from "@reearth/beta/lib/core/mantle"; -import type { Layer, LayerSelectionReason } from "@reearth/beta/lib/core/Map"; +import type { LatLng, ComputedLayer, ComputedFeature } from "@reearth/beta/lib/core/mantle"; +import type { LayerSelectionReason } from "@reearth/beta/lib/core/Map"; import { useLayersFetcher, useSceneFetcher, @@ -15,12 +10,10 @@ import { useStorytellingFetcher, usePropertyFetcher, useLayerStylesFetcher, + useInfoboxFetcher, } from "@reearth/services/api"; import { config } from "@reearth/services/config"; import { - useSceneMode, - useIsCapturing, - useSelectedBlock, useWidgetAlignEditorActivated, useSelectedWidgetArea, useIsVisualizerReady, @@ -33,7 +26,6 @@ import { import { convertWidgets, processLayers, processProperty } from "./convert"; import { convertStory } from "./convert-story"; -import type { BlockType } from "./type"; export default ({ sceneId, @@ -53,15 +45,18 @@ export default ({ const { useCreateStoryBlock, useDeleteStoryBlock } = useStorytellingFetcher(); const { useUpdatePropertyValue, useAddPropertyItem, useMovePropertyItem, useRemovePropertyItem } = usePropertyFetcher(); + const { + useInstallableInfoboxBlocksQuery, + useCreateInfoboxBlock, + useDeleteInfoboxBlock, + useMoveInfoboxBlock, + } = useInfoboxFetcher(); const { nlsLayers } = useGetLayersQuery({ sceneId }); const { layerStyles } = useGetLayerStylesQuery({ sceneId }); const { scene } = useSceneQuery({ sceneId }); - const [sceneMode, setSceneMode] = useSceneMode(); - const [isCapturing, onIsCapturingChange] = useIsCapturing(); - const [selectedBlock, selectBlock] = useSelectedBlock(); const [_, selectSelectedStoryPageId] = useSelectedStoryPageId(); const [widgetAlignEditorActivated] = useWidgetAlignEditorActivated(); const [zoomedLayerId, zoomToLayer] = useZoomedLayerId(); @@ -79,23 +74,30 @@ export default ({ // TODO: Fix to use exact type through GQL typing const sceneProperty = useMemo(() => processProperty(scene?.property), [scene?.property]); - useEffect(() => { - sceneProperty?.default?.sceneMode && setSceneMode(sceneProperty?.default?.sceneMode); - }, [sceneProperty, setSceneMode]); - // Layers const rootLayerId = useMemo(() => scene?.rootLayerId, [scene?.rootLayerId]); + const { installableInfoboxBlocks } = useInstallableInfoboxBlocksQuery({ sceneId }); + + const infoboxBlockNames = useMemo( + () => + installableInfoboxBlocks + ?.map(ib => ({ [ib.extensionId]: ib.name })) + .filter((bn): bn is { [key: string]: string } => !!bn) + .reduce((result, obj) => ({ ...result, ...obj }), {}), + [installableInfoboxBlocks], + ); + const layers = useMemo(() => { - const processedLayers = processLayers(nlsLayers, layerStyles); + const processedLayers = processLayers(nlsLayers, layerStyles, undefined, infoboxBlockNames); if (!showStoryPanel) return processedLayers; return processedLayers?.map(layer => ({ ...layer, visible: true, })); - }, [nlsLayers, layerStyles, showStoryPanel]); + }, [nlsLayers, layerStyles, infoboxBlockNames, showStoryPanel]); - const selectLayer = useCallback( + const handleLayerSelect = useCallback( async ( id?: string, layer?: () => Promise, @@ -115,7 +117,7 @@ export default ({ [selectedLayer, setSelectedLayer, setSelectedLayerStyle, setSelectedSceneSetting], ); - const handleDropLayer = useCallback( + const handleLayerDrop = useCallback( async (_propertyId: string, propertyKey: string, _position?: LatLng) => { // propertyKey will be "default.location" for example const [_schemaGroupId, _fieldId] = propertyKey.split(".", 2); @@ -126,14 +128,14 @@ export default ({ // Widgets const widgets = convertWidgets(scene); - const onWidgetUpdate = useCallback( + const handleWidgetUpdate = useCallback( async (id: string, update: { location?: Location; extended?: boolean; index?: number }) => { await useUpdateWidget(id, update, sceneId); }, [sceneId, useUpdateWidget], ); - const onWidgetAlignSystemUpdate = useCallback( + const handleWidgetAlignSystemUpdate = useCallback( async (location: Location, align: Alignment) => { await useUpdateWidgetAlignSystem( { zone: location.zone, section: location.section, area: location.area, align }, @@ -153,54 +155,46 @@ export default ({ [scene?.plugins], ); - // Infobox - NOTE: this is from classic. TBD but will change significantly - const onBlockChange = useCallback( - async ( - blockId: string, - _schemaGroupId: string, - _fid: string, - _v: ValueTypes[T], - vt: T, - selectedLayer?: Layer, - ) => { - const propertyId = (selectedLayer?.infobox?.blocks?.find(b => b.id === blockId) as any) - ?.propertyId as string | undefined; - if (!propertyId) return; - - console.log("Block has been changed!"); + const handleInfoboxBlockCreate = useCallback( + async (pluginId: string, extensionId: string, index?: number) => { + if (!selectedLayer) return; + await useCreateInfoboxBlock({ + layerId: selectedLayer.layerId, + pluginId, + extensionId, + index, + }); }, - [], + [selectedLayer, useCreateInfoboxBlock], ); - const onBlockMove = useCallback( - async (_id: string, _fromIndex: number, _toIndex: number) => { + const handleInfoboxBlockMove = useCallback( + async (id: string, targetIndex: number) => { if (!selectedLayer) return; - console.log("Block has been moved!"); + await useMoveInfoboxBlock({ + layerId: selectedLayer.layerId, + infoboxBlockId: id, + index: targetIndex, + }); }, - [selectedLayer], + [selectedLayer, useMoveInfoboxBlock], ); - const onBlockRemove = useCallback( - async (_id: string) => { - if (!selectedLayer) return; - console.log("Block has been removed!"); + const handleInfoboxBlockRemove = useCallback( + async (id?: string) => { + if (!selectedLayer || !id) return; + await useDeleteInfoboxBlock({ + layerId: selectedLayer.layerId, + infoboxBlockId: id, + }); }, - [selectedLayer], + [selectedLayer, useDeleteInfoboxBlock], ); - // block selector - const blocks: BlockType[] = useMemo(() => [], []); - const onBlockInsert = (bi: number, _i: number, _p?: "top" | "bottom") => { - const b = blocks?.[bi]; - if (b?.pluginId && b?.extensionId && selectedLayer) { - console.log("Block has been inserted!"); - } - }; - // Story const story = useMemo(() => convertStory(scene, storyId), [storyId, scene]); - const handleCurrentPageChange = useCallback( + const handleStoryPageChange = useCallback( (pageId?: string) => selectSelectedStoryPageId(pageId), [selectSelectedStoryPageId], ); @@ -281,41 +275,34 @@ export default ({ }, [isBuilt, title]); return { - sceneId, rootLayerId, - selectedBlockId: selectedBlock, sceneProperty, pluginProperty, - widgets, layers, + widgets, story, - blocks, - isCapturing, - sceneMode, selectedWidgetArea, widgetAlignEditorActivated, engineMeta, useExperimentalSandbox: false, // TODO: test and use new sandbox in beta solely, removing old way too. - isVisualizerReady, - selectWidgetArea: setSelectedWidgetArea, + isVisualizerReady, // Not being used (as of 2024/04) zoomedLayerId, - handleCurrentPageChange, + installableInfoboxBlocks, + handleLayerSelect, + handleLayerDrop, + handleStoryPageChange, handleStoryBlockCreate, handleStoryBlockDelete, + handleInfoboxBlockCreate, + handleInfoboxBlockMove, + handleInfoboxBlockRemove, + handleWidgetUpdate, + handleWidgetAlignSystemUpdate, + selectWidgetArea: setSelectedWidgetArea, handlePropertyValueUpdate, handlePropertyItemAdd, handlePropertyItemDelete, handlePropertyItemMove, - selectLayer, - selectBlock, - onBlockChange, - onBlockMove, - onBlockRemove, - onBlockInsert, - onWidgetUpdate, - onWidgetAlignSystemUpdate, - onIsCapturingChange, - handleDropLayer, handleMount, zoomToLayer, }; diff --git a/web/src/beta/features/Editor/Visualizer/index.tsx b/web/src/beta/features/Editor/Visualizer/index.tsx index 8ae15347cb..c0f0c5b8f3 100644 --- a/web/src/beta/features/Editor/Visualizer/index.tsx +++ b/web/src/beta/features/Editor/Visualizer/index.tsx @@ -1,7 +1,6 @@ -import { MutableRefObject, useCallback } from "react"; +import { MutableRefObject } from "react"; -import ContentPicker from "@reearth/beta/components/ContentPicker"; -import { InteractionModeType } from "@reearth/beta/lib/core/Crust"; +import { type InteractionModeType } from "@reearth/beta/lib/core/Crust"; import type { MapRef } from "@reearth/beta/lib/core/Map/ref"; import { SketchFeature, SketchType } from "@reearth/beta/lib/core/Map/Sketch/types"; import type { SceneProperty } from "@reearth/beta/lib/core/Map/types"; @@ -9,7 +8,6 @@ import StoryPanel, { StoryPanelRef, type InstallableStoryBlock, } from "@reearth/beta/lib/core/StoryPanel"; -import { type Props as VisualizerProps } from "@reearth/beta/lib/core/Visualizer"; import CoreVisualizer from "@reearth/beta/lib/core/Visualizer"; import type { Camera } from "@reearth/beta/utils/value"; import type { Story } from "@reearth/services/api/storytellingApi/utils"; @@ -29,7 +27,7 @@ export type Props = { storyPanelRef?: MutableRefObject; showStoryPanel?: boolean; selectedStory?: Story; - installableBlocks?: InstallableStoryBlock[]; + installableStoryBlocks?: InstallableStoryBlock[]; onStoryBlockMove: (id: string, targetId: number, blockId: string) => void; onCameraChange: (camera: Camera) => void; onSketchTypeChange?: (type: SketchType | undefined) => void; @@ -46,7 +44,7 @@ const Visualizer: React.FC = ({ storyPanelRef, showStoryPanel, selectedStory, - installableBlocks, + installableStoryBlocks, onStoryBlockMove, onCameraChange, onSketchTypeChange, @@ -54,49 +52,36 @@ const Visualizer: React.FC = ({ }) => { const { rootLayerId, - selectedBlockId, sceneProperty, pluginProperty, layers, widgets, story, - blocks, selectedWidgetArea, widgetAlignEditorActivated, engineMeta, useExperimentalSandbox, - isVisualizerReady: _isVisualizerReady, zoomedLayerId, - handleCurrentPageChange, + installableInfoboxBlocks, + handleLayerSelect, + handleLayerDrop, + handleStoryPageChange, handleStoryBlockCreate, handleStoryBlockDelete, + handleInfoboxBlockCreate, + handleInfoboxBlockMove, + handleInfoboxBlockRemove, + handleWidgetUpdate, + handleWidgetAlignSystemUpdate, + selectWidgetArea, handlePropertyValueUpdate, handlePropertyItemAdd, handlePropertyItemDelete, handlePropertyItemMove, - selectLayer, - selectBlock, - onBlockChange, - onBlockMove, - onBlockRemove, - onBlockInsert, - onWidgetUpdate, - onWidgetAlignSystemUpdate, - selectWidgetArea, - handleDropLayer, handleMount, zoomToLayer, } = useHooks({ sceneId, isBuilt, storyId: selectedStory?.id, showStoryPanel }); - const renderInfoboxInsertionPopUp = useCallback< - NonNullable - >( - (onSelect, onClose) => ( - - ), - [blocks], - ); - return ( = ({ isBuilt={!!isBuilt} inEditor={!!inEditor} layers={layers} + installableInfoboxBlocks={installableInfoboxBlocks} widgetAlignSystem={widgets?.alignSystem} floatingWidgets={widgets?.floating} widgetLayoutConstraint={widgets?.layoutConstraint} ownBuiltinWidgets={widgets?.ownBuiltinWidgets} - selectedBlockId={selectedBlockId} selectedWidgetArea={selectedWidgetArea} zoomedLayerId={zoomedLayerId} rootLayerId={rootLayerId} @@ -125,28 +110,29 @@ const Visualizer: React.FC = ({ storyPanelPosition={story?.position} interactionMode={interactionMode} onCameraChange={onCameraChange} - onLayerSelect={selectLayer} - onWidgetLayoutUpdate={onWidgetUpdate} - onWidgetAlignmentUpdate={onWidgetAlignSystemUpdate} + onLayerSelect={handleLayerSelect} + onLayerDrop={handleLayerDrop} + onWidgetLayoutUpdate={handleWidgetUpdate} + onWidgetAlignmentUpdate={handleWidgetAlignSystemUpdate} onWidgetAreaSelect={selectWidgetArea} - onBlockSelect={selectBlock} - onBlockChange={onBlockChange} - onBlockMove={onBlockMove} - onBlockDelete={onBlockRemove} - onBlockInsert={onBlockInsert} - onLayerDrop={handleDropLayer} + onInfoboxBlockCreate={handleInfoboxBlockCreate} + onInfoboxBlockMove={handleInfoboxBlockMove} + onInfoboxBlockDelete={handleInfoboxBlockRemove} + onPropertyUpdate={handlePropertyValueUpdate} + onPropertyItemAdd={handlePropertyItemAdd} + onPropertyItemMove={handlePropertyItemMove} + onPropertyItemDelete={handlePropertyItemDelete} onZoomToLayer={zoomToLayer} - onMount={handleMount} onSketchTypeChange={onSketchTypeChange} onSketchFeatureCreate={onSketchFeatureCreate} - renderInfoboxInsertionPopup={renderInfoboxInsertionPopUp}> + onMount={handleMount}> {showStoryPanel && ( { + const schema = orig?.schema || parent?.schema; + if (!schema) return; + + const allItems: Record< + string, + { + schema: PropertySchemaGroupFragmentFragment; + orig?: PropertyItemFragmentFragment; + parent?: PropertyItemFragmentFragment; + } + > = schema.groups.reduce( + (a, b) => ({ + ...a, + [b.schemaGroupId]: { + schema: b, + orig: orig?.items.find(i => i.schemaGroupId === b.schemaGroupId), + parent: parent?.items.find(i => i.schemaGroupId === b.schemaGroupId), + }, + }), + {}, + ); + const mergedProperty: P = Object.fromEntries( + Object.entries(allItems) + .map(([key, value]) => { + const { schema, orig, parent } = value; + if (!orig && !parent) { + if (schema.isList) { + return [key, undefined]; + } + return [ + key, + processPropertyGroups(schema, undefined, undefined, linkedDatasetId, datasets), + ]; + } + + if ( + (!orig || orig.__typename === "PropertyGroupList") && + (!parent || parent.__typename === "PropertyGroupList") + ) { + const used = orig || parent; + return [ + key, + used?.groups.map(g => ({ + ...processPropertyGroups(schema, g, undefined, linkedDatasetId, datasets), + id: g.id, + })), + ]; + } + + if ( + (!orig || orig.__typename === "PropertyGroup") && + (!parent || parent.__typename === "PropertyGroup") + ) { + return [key, processPropertyGroups(schema, parent, orig, linkedDatasetId, datasets)]; + } + return [key, null]; + }) + .filter(([, value]) => !!value), + ); + + return mergedProperty; +}; + +const processPropertyGroups = ( + schema: PropertySchemaGroupFragmentFragment, + parent: PropertyGroupFragmentFragment | null | undefined, + original: PropertyGroupFragmentFragment | null | undefined, + linkedDatasetId: string | null | undefined, + datasets: DatasetMap | null | undefined, +): any => { + const allFields: Record< + string, + { + schema: PropertySchemaFieldFragmentFragment; + parent?: PropertyFieldFragmentFragment; + orig?: PropertyFieldFragmentFragment; + } + > = schema.fields.reduce( + (a, b) => ({ + ...a, + [b.fieldId]: { + schema: b, + parent: parent?.fields.find(i => i.fieldId === b.fieldId), + orig: original?.fields.find(i => i.fieldId === b.fieldId), + }, + }), + {}, + ); + + return Object.fromEntries( + Object.entries(allFields).map(([key, { schema, parent, orig }]) => { + const used = orig || parent; + + const fieldMeta = { + type: valueTypeFromGQL(schema.type) || undefined, + ui: toUi(schema.ui) || undefined, + title: schema.translatedTitle || undefined, + description: schema.translatedDescription || undefined, + choices: schema.choices || undefined, + min: schema.min || undefined, + max: schema.max || undefined, + }; + + if (!used) { + return [ + key, + { + ...fieldMeta, + value: schema.defaultValue + ? valueFromGQL(schema.defaultValue, schema.type)?.value + : undefined, + }, + ]; + } + + const datasetSchemaId = used?.links?.[0]?.datasetSchemaId; + const datasetFieldId = used?.links?.[0]?.datasetSchemaFieldId; + if (datasetSchemaId && linkedDatasetId && datasetFieldId) { + return [ + key, + { + ...fieldMeta, + value: datasetValue(datasets, datasetSchemaId, linkedDatasetId, datasetFieldId), + }, + ]; + } + + return [ + key, + { + ...fieldMeta, + value: valueFromGQL(used.value, used.type)?.value, + }, + ]; + }), + ); +}; diff --git a/web/src/beta/features/Editor/index.tsx b/web/src/beta/features/Editor/index.tsx index afc4c56f41..aa67e15dce 100644 --- a/web/src/beta/features/Editor/index.tsx +++ b/web/src/beta/features/Editor/index.tsx @@ -79,6 +79,7 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { isVisualizerReady, visualizerRef, }); + const { scene, selectedSceneSetting, sceneSettings, handleSceneSettingSelect } = useScene({ sceneId, }); @@ -230,7 +231,7 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { sceneId={sceneId} showStoryPanel={selectedProjectType === "story"} selectedStory={selectedStory} - installableBlocks={installableStoryBlocks} + installableStoryBlocks={installableStoryBlocks} currentCamera={currentCamera} onStoryBlockMove={onStoryBlockMove} onCameraChange={handleCameraUpdate} @@ -297,7 +298,6 @@ const MainSection = styled.div` display: flex; flex-grow: 1; height: 0; - background: ${({ theme }) => theme.bg[0]}; `; const Center = styled.div` @@ -318,7 +318,9 @@ const VisualizerWrapper = styled.div<{ }>` flex: 1; min-height: 0; - border-radius: 4px; + padding: 2px; width: ${({ visualizerWidth }) => - typeof visualizerWidth === "number" ? `${visualizerWidth}px` : visualizerWidth}; + typeof visualizerWidth === "number" + ? `calc(${visualizerWidth} - 4px)` + : `calc(${visualizerWidth} - 4px)`}; `; diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/FeatureData.tsx b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/FeatureData.tsx index ea00862e19..dc329c013d 100644 --- a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/FeatureData.tsx +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/FeatureData.tsx @@ -28,7 +28,12 @@ const FeatureData: React.FC = ({ selectedFeature }) => { {t("Geometry")} - + {t("Properties")} diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/index.tsx b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/index.tsx index b2e6decc30..b32ae7f587 100644 --- a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/index.tsx +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/index.tsx @@ -9,6 +9,7 @@ import { SelectedLayer } from "@reearth/services/state"; import { LayerConfigUpdateProps } from "../../../../useLayers"; import FeatureData from "./FeatureData"; +import Infobox from "./infobox"; import LayerData from "./LayerData"; import LayerTab from "./LayerStyle"; @@ -65,7 +66,7 @@ const InspectorTabs: React.FC = ({ id: "featureData", name: t("Feature"), component: selectedFeature && , - icon: "location", + icon: "marker", }, { id: "layerStyleSelector", @@ -81,13 +82,13 @@ const InspectorTabs: React.FC = ({ ), icon: "layerStyle", }, - // TODO: new beta infobox implementation - // { - // id: "infobox", - // name: t("Infobox"), - // component:
TODO
, - // icon: "infobox", - // }, + { + id: "infobox", + component: selectedLayer && ( + + ), + icon: "infobox", + }, ], [sceneId, selectedLayer, selectedFeature, layerStyles, layers, t, onLayerConfigUpdate], ); diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/hooks.ts b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/hooks.ts new file mode 100644 index 0000000000..1a528053d5 --- /dev/null +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/hooks.ts @@ -0,0 +1,26 @@ +import { useCallback, useMemo } from "react"; + +import { filterVisibleItems } from "@reearth/beta/components/fields/utils"; +import { useInfoboxFetcher } from "@reearth/services/api"; +import { Item, convert } from "@reearth/services/api/propertyApi/utils"; + +export default ({ layerId, property }: { layerId: string; property?: any }) => { + const { useCreateNLSInfobox } = useInfoboxFetcher(); + // const { useUpdatePropertyValue } = usePropertyFetcher(); + + const visibleItems: Item[] | undefined = useMemo( + () => filterVisibleItems(convert(property)), + [property], + ); + + const handleInfoboxCreate = useCallback(async () => { + if (!property) { + await useCreateNLSInfobox({ layerId }); + } + }, [layerId, property, useCreateNLSInfobox]); + + return { + visibleItems, + handleInfoboxCreate, + }; +}; diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/index.tsx b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/index.tsx new file mode 100644 index 0000000000..990d6fe3f6 --- /dev/null +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/LayerInspector/infobox/index.tsx @@ -0,0 +1,46 @@ +import PropertyItem from "@reearth/beta/components/fields/Property/PropertyItem"; +import ToggleField from "@reearth/beta/components/fields/ToggleField"; +import { NLSInfobox } from "@reearth/services/api/layersApi/utils"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import useHooks from "./hooks"; + +type Props = { + selectedLayerId: string; + infobox?: NLSInfobox; +}; + +const Infobox: React.FC = ({ selectedLayerId, infobox }) => { + const t = useT(); + + const { visibleItems, handleInfoboxCreate } = useHooks({ + layerId: selectedLayerId, + property: infobox?.property, + }); + + return ( + + {visibleItems ? ( + visibleItems.map(i => ( + + )) + ) : ( + + )} + + ); +}; + +export default Infobox; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx b/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx index 80cb629200..36c0de5f72 100644 --- a/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx @@ -52,6 +52,7 @@ const MapRightPanel: React.FC = ({ return ( void; }; const Toolbar: React.FC = ({ enable, sketchType, onSketchTypeChange }) => { + const sketchTools = useMemo( + () => [ + { + icon: "marker", + selected: enable && sketchType === "marker", + onClick: () => onSketchTypeChange("marker"), + }, + { + icon: "polyline", + selected: enable && sketchType === "polyline", + onClick: () => onSketchTypeChange("polyline"), + }, + { + icon: "circleOutline", + selected: enable && sketchType === "circle", + onClick: () => onSketchTypeChange("circle"), + }, + { + icon: "squareOutline", + selected: enable && sketchType === "rectangle", + onClick: () => onSketchTypeChange("rectangle"), + }, + { + icon: "polygon", + selected: enable && sketchType === "polygon", + onClick: () => onSketchTypeChange("polygon"), + }, + { + icon: "cylinder", + selected: enable && sketchType === "extrudedCircle", + onClick: () => onSketchTypeChange("extrudedCircle"), + }, + { + icon: "box", + selected: enable && sketchType === "extrudedRectangle", + onClick: () => onSketchTypeChange("extrudedRectangle"), + }, + { + icon: "polygonExtruded", + selected: enable && sketchType === "extrudedPolygon", + onClick: () => onSketchTypeChange("extrudedPolygon"), + }, + ], + [enable, sketchType, onSketchTypeChange], + ); + useEffect(() => { if (!enable) onSketchTypeChange(undefined); }, [enable, onSketchTypeChange]); return ( - - - onSketchTypeChange("marker")} - /> - onSketchTypeChange("polyline")} - /> - onSketchTypeChange("circle")} - /> - onSketchTypeChange("rectangle")} - /> - onSketchTypeChange("polygon")} - /> + + {sketchTools.map(({ icon, selected, onClick }) => ( onSketchTypeChange("extrudedCircle")} + selected={selected} + onClick={onClick} /> - onSketchTypeChange("extrudedRectangle")} - /> - onSketchTypeChange("extrudedPolygon")} - /> - - + ))} + ); }; export default Toolbar; -const StyledSecondaryNav = styled(SecondaryNav)` +const Wrapper = styled.div` display: flex; justify-content: flex-start; align-items: center; - gap: 4px; - padding-right: 4px; - padding-left: 4px; -`; - -const ButtonGroup = styled.div` - display: flex; - align-items: center; - box-sizing: border-box; - height: 36px; + height: 32px; gap: 8px; - padding: 4px; - border-left: 2px solid ${({ theme }) => theme.outline.weaker}; - - &:first-of-type { - border-left: none; - } + padding-left: 4px; + padding-right: 4px; + margin: 2px 1px 1px 1px; + border-radius: 8px; + background: ${({ theme }) => theme.bg[0]}; `; const ToolButton = styled(Button)<{ selected?: boolean }>` - padding: 10px; + height: 24px; + width: 24px; + padding: 0; border: none; box-shadow: none; background-color: ${({ theme, selected }) => (selected ? theme.select.main : "none")}; diff --git a/web/src/beta/features/Editor/tabs/story/LeftPanel/index.tsx b/web/src/beta/features/Editor/tabs/story/LeftPanel/index.tsx index 72290efac5..af0df6c4ba 100644 --- a/web/src/beta/features/Editor/tabs/story/LeftPanel/index.tsx +++ b/web/src/beta/features/Editor/tabs/story/LeftPanel/index.tsx @@ -37,6 +37,7 @@ const StoryLeftPanel: React.FC = ({ return ( = ({ return ( void; - children?: React.ReactNode; -} - -const AdditionButton: React.FC = ({ className, children, disabled, onClick }) => { - const referenceElement = useRef(null); - const popperElement = useRef(null); - const { - styles, - attributes, - update: updatePopper, - } = usePopper(referenceElement.current, popperElement.current, { - placement: "bottom", - strategy: "fixed", - modifiers: [ - { - name: "eventListeners", - enabled: false, - options: { - scroll: false, - resize: false, - }, - }, - ], - }); - - const handleClick = useCallback(() => { - if (disabled) return; - onClick?.(); - }, [disabled, onClick]); - - // TODO: わかりずらい。もっといい方法ありそう。 - useEffect(() => { - if (children) { - updatePopper?.(); - } - }, [children, updatePopper]); - - return ( - - - - - - - -
- {children} -
-
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - &:hover { - * { - visibility: visible; - opacity: 1; - } - } -`; - -const InsertArea = styled.div` - width: 100%; - padding: 0 0 30px 0; - display: flex; - align-items: center; - justify-content: center; - visibility: hidden; - opacity: 0; - transition: all 0.5s; -`; - -const StyledIcon = styled(Icon)` - color: ${props => props.theme.select.main}; -`; - -const Button = styled.div` - color: ${props => props.theme.select.main}; - margin: 0 3px; -`; - -const Line = styled.div` - width: 43%; - background-color: ${props => props.theme.select.main}; - height: 2px; - margin-top: -2px; -`; - -export default AdditionButton; diff --git a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.stories.tsx b/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.stories.tsx deleted file mode 100644 index 2887df21fd..0000000000 --- a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.stories.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Meta, Story } from "@storybook/react"; - -import DataList, { Item, Props } from "."; - -export default { - component: DataList, - parameters: { actions: { argTypesRegex: "^on.*" } }, -} as Meta; - -const items: Item[] = [ - { id: "a", item_title: "Name", item_datastr: "Foo bar", item_datatype: "string" }, - { id: "b", item_title: "Age", item_datanum: 20, item_datatype: "number" }, - { id: "c", item_title: "Address", item_datastr: "New York", item_datatype: "string" }, -]; - -const Template: Story = args => ; - -export const Default = Template.bind({}); -Default.args = { - block: { id: "", property: { items } }, -}; - -export const Title = Template.bind({}); -Title.args = { - block: { id: "", property: { title: "Title", items } }, -}; - -export const Typography: Story = Template.bind({}); -Typography.args = { - block: { - id: "", - property: { - typography: { color: "red", fontSize: 16 }, - items, - }, - }, -}; - -export const NoItems: Story = Template.bind({}); -NoItems.args = { isEditable: true }; - -export const Selected: Story = Template.bind({}); -Selected.args = { block: { id: "", property: { items } }, isSelected: true }; - -export const Editable: Story = Template.bind({}); -Editable.args = { - block: { id: "", property: { items } }, - isSelected: true, - isEditable: true, -}; - -export const Built: Story = Template.bind({}); -Built.args = { - block: { id: "", property: { items } }, - isBuilt: true, -}; diff --git a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.tsx b/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.tsx deleted file mode 100644 index d587c52985..0000000000 --- a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/DataList/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { Fragment, useCallback, useState } from "react"; - -import Icon from "@reearth/beta/components/Icon"; -import { Typography, typographyStyles } from "@reearth/beta/utils/value"; -import { styled } from "@reearth/services/theme"; - -import { CommonProps as BlockProps } from "../.."; -import { Border, Title } from "../../utils"; - -export type Props = BlockProps; - -export type Item = { - id: string; - item_title?: string; - item_datatype?: "string" | "number"; - item_datastr?: string; - item_datanum?: number; -}; - -export type Property = { - title?: string; - typography?: Typography; - items?: Item[]; -}; - -const DataList: React.FC = ({ block, infoboxProperty, isSelected, isEditable, onClick }) => { - const { items } = block?.property ?? {}; - const { title, typography } = block?.property ?? {}; - const isTemplate = !title && !items; - - const [isHovered, setHovered] = useState(false); - const handleMouseEnter = useCallback(() => setHovered(true), []); - const handleMouseLeave = useCallback(() => setHovered(false), []); - const handleClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - onClick?.(); - }, - [onClick], - ); - - return ( - - {isTemplate && isEditable ? ( - - ) : ( - <> - {title && {title}} -
- {items?.map(i => ( - -
{i.item_title}
-
{i.item_datatype === "number" ? i.item_datanum : i.item_datastr}
-
- ))} -
- - )} -
- ); -}; - -const Wrapper = styled(Border)<{ - typography?: Typography; -}>` - margin: 0 8px; - ${({ typography }) => typographyStyles({ ...typography })} - min-height: 70px; -`; - -const Dl = styled.dl` - display: flex; - flex-wrap: wrap; - min-height: 15px; -`; - -const Dt = styled.dt` - width: 30%; - padding: 10px; - padding-left: 0; - box-sizing: border-box; - font-weight: bold; - word-break: break-all; -`; - -const Dd = styled.dd` - width: 70%; - margin: 0; - padding: 10px; - padding-right: 0; - box-sizing: border-box; -`; - -const Template = styled.div` - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; - width: 100%; - height: 185px; - margin: 0 auto; - user-select: none; -`; - -const StyledIcon = styled(Icon)<{ isSelected?: boolean; isHovered?: boolean }>` - color: ${props => - props.isHovered - ? props.theme.outline.main - : props.isSelected - ? props.theme.select.main - : props.theme.content.weak}; -`; - -export default DataList; diff --git a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.stories.tsx b/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.stories.tsx deleted file mode 100644 index 576ec6bdc7..0000000000 --- a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Meta, Story } from "@storybook/react"; - -import HTML, { Props } from "."; - -export default { - component: HTML, - parameters: { actions: { argTypesRegex: "^on.*" } }, -} as Meta; - -const Template: Story = args => ; - -export const Default = Template.bind({}); -Default.args = { - block: { id: "", property: { html: "

aaaaaa

" } }, - isSelected: false, - isBuilt: false, - isEditable: false, -}; - -export const Title = Template.bind({}); -Title.args = { - block: { id: "", property: { html: "

aaaaaa

", title: "Title" } }, - isSelected: false, - isBuilt: false, - isEditable: false, -}; - -export const NoText = Template.bind({}); -NoText.args = { - isSelected: false, - isBuilt: false, - isEditable: true, -}; diff --git a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.tsx b/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.tsx deleted file mode 100644 index e9efc3c51b..0000000000 --- a/web/src/beta/lib/core/Crust/Infobox/Block/builtin/HTML/index.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from "react"; - -import Icon from "@reearth/beta/components/Icon"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; -import fonts from "@reearth/services/theme/reearthTheme/common/fonts"; - -import { CommonProps as BlockProps } from "../.."; -import { Border } from "../../utils"; - -export type Props = BlockProps; - -export type Property = { - html?: string; - title?: string; -}; - -const HTMLBlock: React.FC = ({ - block, - isSelected, - isEditable, - infoboxProperty, - theme, - onChange, - onClick, -}) => { - const t = useT(); - const { html, title } = block?.property ?? {}; - - const ref = useRef(null); - const isDirty = useRef(false); - const [editingText, setEditingText] = useState(); - const isEditing = typeof editingText === "string"; - const isTemplate = !html && !title && !isEditing; - - const startEditing = useCallback(() => { - if (!isEditable) return; - setEditingText(html ?? ""); - }, [isEditable, html]); - - const finishEditing = useCallback(() => { - if (!isEditing) return; - if (onChange && isDirty.current) { - onChange("default", "html", editingText ?? "", "string"); - } - isDirty.current = false; - setEditingText(undefined); - }, [editingText, onChange, isEditing]); - - const handleChange = useCallback( - (e: React.ChangeEvent) => { - setEditingText(e.currentTarget.value); - isDirty.current = true; - }, - [], - ); - - useEffect(() => { - if (isEditing) { - ref.current?.focus(); - } - }, [isEditing]); - - const isSelectedPrev = useRef(false); - useEffect(() => { - if (isEditing && !isSelected && isSelectedPrev.current) { - finishEditing(); - } - }, [finishEditing, isSelected, isEditing]); - useEffect(() => { - isSelectedPrev.current = !!isSelected; - }, [isSelected]); - - const [isHovered, setHovered] = useState(false); - const handleMouseEnter = useCallback(() => setHovered(true), []); - const handleMouseLeave = useCallback(() => setHovered(false), []); - const handleClick = useCallback( - (e?: React.MouseEvent) => { - e?.stopPropagation(); - if (isEditing) return; - onClick?.(); - }, - [isEditing, onClick], - ); - - // iframe - const themeColor = infoboxProperty?.typography?.color ?? theme?.mainText; - const [frameRef, setFrameRef] = useState(null); - const [height, setHeight] = useState(15); - const initializeIframe = useCallback(() => { - const frameDocument = frameRef?.contentDocument; - const frameWindow = frameRef?.contentWindow; - if (!frameWindow || !frameDocument) { - return; - } - - if (!frameDocument.body.innerHTML.length) { - // `document.write()` is not recommended API by HTML spec, - // but we need to use this API to make it work correctly on Safari. - // If Safari supports `onLoad` event with `srcDoc`, we can remove this line. - frameDocument.write(html || ""); - } - - // Initialize styles - frameWindow.document.documentElement.style.margin = "0"; - - // Check if a style element has already been appended to the head - let style: HTMLElement | null = frameWindow.document.querySelector( - 'style[data-id="reearth-iframe-style"]', - ); - if (!style) { - // Create a new style element if it doesn't exist - style = frameWindow.document.createElement("style"); - style.dataset.id = "reearth-iframe-style"; - frameWindow.document.head.append(style); - } - // Update the content of the existing or new style element - style.textContent = `body { color:${themeColor ?? getComputedStyle(frameRef).color}; - font-family:Noto Sans, hiragino sans, hiragino kaku gothic proN, -apple-system, BlinkMacSystem, sans-serif; - font-size: ${fonts.sizes.body}px; } a { color:${ - themeColor ?? getComputedStyle(frameRef).color - };}`; - - const handleFrameClick = () => handleClick(); - - if (isEditable) { - frameWindow.document.body.style.cursor = "pointer"; - frameWindow.document.addEventListener("dblclick", startEditing); - frameWindow.document.addEventListener("click", handleFrameClick); - } - - const resize = () => { - setHeight(frameWindow.document.documentElement.scrollHeight); - }; - - // Resize - const resizeObserver = new ResizeObserver(() => { - resize(); - }); - resizeObserver.observe(frameWindow.document.body); - - return () => { - frameWindow.document.removeEventListener("dblclick", startEditing); - frameWindow.document.removeEventListener("click", handleFrameClick); - resizeObserver.disconnect(); - }; - }, [frameRef, themeColor, isEditable, html, handleClick, startEditing]); - - useLayoutEffect(() => initializeIframe(), [initializeIframe]); - - return ( - - {isTemplate && isEditable && !isEditing ? ( - - ) : ( - <> - {title && {title}} - {isEditing ? ( - - ) : ( -