From 8315f2ac1e72d540b3be134943a67e23c83ab48d Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Mon, 10 Aug 2026 09:44:44 +0200 Subject: [PATCH 01/43] WIP: Refactor screenspace renderable list to work more like scene menu --- .../ScreenSpaceRenderableListItem.tsx | 71 +++++++++++++++ .../ScreenSpaceRenderablePanel.tsx | 88 +++++++++++-------- .../ScreenSpaceRenderableView.tsx | 82 +++++++++++++++++ 3 files changed, 203 insertions(+), 38 deletions(-) create mode 100644 src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableListItem.tsx create mode 100644 src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableView.tsx diff --git a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableListItem.tsx b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableListItem.tsx new file mode 100644 index 000000000..69b8b8434 --- /dev/null +++ b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableListItem.tsx @@ -0,0 +1,71 @@ +import { ActionIcon, Button } from '@mantine/core'; + +import { useOpenSpaceApi } from '@/api/hooks'; +import { PropertyOwnerVisibilityCheckbox } from '@/components/PropertyOwner/VisiblityCheckbox'; +import { ThreePartHeader } from '@/components/ThreePartHeader/ThreePartHeader'; +import { TruncatedText } from '@/components/TruncatedText/TruncatedText'; +import { usePropertyOwner, usePropertyOwnerVisibility } from '@/hooks/propertyOwner'; +import { MinusIcon } from '@/icons/icons'; +import { Uri } from '@/types/types'; + +interface Props { + uri: Uri; + onClick?: () => void; +} + +export function ScreenSpaceRenderableListItem({ uri, onClick }: Props) { + const propertyOwner = usePropertyOwner(uri); + + if (!propertyOwner) { + throw Error(`No property owner found for uri: ${uri}`); + } + + const { visibility, setVisibility } = usePropertyOwnerVisibility(uri); + + const luaApi = useOpenSpaceApi(); + + function removeSlide(uri: Uri) { + const identifier = uri.split('.').pop(); + + if (!identifier) { + return; + } + + luaApi?.removeScreenSpaceRenderable(identifier); + } + + return ( + + {propertyOwner.name} + + } + leftSection={ + + } + rightSection={ + removeSlide(uri)} + color={'red'} + variant={'outline'} + size={'sm'} + aria-label={`Remove : ${uri})`} // TODO: i18n + > + + + } + /> + ); +} diff --git a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx index 37f6ca9df..6dc179479 100644 --- a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx @@ -1,34 +1,28 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ActionIcon, Box, Divider, Group, Tabs, Text } from '@mantine/core'; +import { Box, Divider, Tabs, Text } from '@mantine/core'; -import { useOpenSpaceApi } from '@/api/hooks'; -import { PropertyOwner } from '@/components/PropertyOwner/PropertyOwner'; +import { ResizeableContent } from '@/components/ResizeableContent/ResizeableContent'; +import { ScrollBox } from '@/components/ScrollBox/ScrollBox'; import { usePropertyOwner } from '@/hooks/propertyOwner'; -import { InsertPhotoIcon, MinusIcon, WebIcon } from '@/icons/icons'; +import { InsertPhotoIcon, WebIcon } from '@/icons/icons'; import { IconSize } from '@/types/enums'; import { Uri } from '@/types/types'; import { ScreenSpaceKey } from '@/util/keys'; import { ImageTab } from './ImageTab'; +import { ScreenSpaceRenderableListItem } from './ScreenSpaceRenderableListItem'; +import { ScreenSpaceRenderableView } from './ScreenSpaceRenderableView'; import { WebpageTab } from './WebpageTab'; export function ScreenSpaceRenderablePanel() { const { t } = useTranslation('panel-screenspacerenderable'); - const luaApi = useOpenSpaceApi(); const screenSpacePropertyOwner = usePropertyOwner(ScreenSpaceKey); - const renderables = screenSpacePropertyOwner?.subowners ?? []; - - function removeSlide(uri: Uri) { - const identifier = uri.split('.').pop(); - - if (!identifier) { - return; - } + const [selectedRenderable, setSelectedRenderable] = useState(null); - luaApi?.removeScreenSpaceRenderable(identifier); - } + const renderables = screenSpacePropertyOwner?.subowners ?? []; return ( <> @@ -42,7 +36,7 @@ export function ScreenSpaceRenderablePanel() { - + @@ -53,31 +47,49 @@ export function ScreenSpaceRenderablePanel() { + {renderables.length === 0 ? ( {t('added-slides.empty-slides')} ) : ( - renderables.map((uri) => ( - - - - - removeSlide(uri)} - color={'red'} - variant={'outline'} - aria-label={`${t('added-slides.remove-slide-aria-label')}: ${uri})`} - > - - - - )) + <> + + + {renderables.map((uri) => ( + + + selectedRenderable == uri + ? setSelectedRenderable(null) + : setSelectedRenderable(uri) + } + /> + + ))} + + + + {selectedRenderable ? ( + + ) : ( + Select an item to view its details + )} + + )} ); diff --git a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableView.tsx b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableView.tsx new file mode 100644 index 000000000..fa6edecd0 --- /dev/null +++ b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderableView.tsx @@ -0,0 +1,82 @@ +import { Box, Tabs, Tooltip } from '@mantine/core'; + +import { PropertyOwner } from '@/components/PropertyOwner/PropertyOwner'; +import { PropertyOwnerVisibilityCheckbox } from '@/components/PropertyOwner/VisiblityCheckbox'; +import { ThreePartHeader } from '@/components/ThreePartHeader/ThreePartHeader'; +import { usePropertyOwner, usePropertyOwnerVisibility } from '@/hooks/propertyOwner'; +import { Uri } from '@/types/types'; + +interface Props { + uri: Uri; +} + +export function ScreenSpaceRenderableView({ uri }: Props) { + // const { t } = useTranslation('panel-scene', { + // keyPrefix: 'scene-graph-node.node-view' + // }); + + const propertyOwner = usePropertyOwner(uri); + + // Extract some custom propertyowners + const placementOwner = usePropertyOwner(`${uri}.Placement`); + const styleOwner = usePropertyOwner(`${uri}.Style`); + + const { visibility, setVisibility } = usePropertyOwnerVisibility(uri); + + if (!propertyOwner) { + return {/* {t('not-found-info')} */}; + } + + if (!placementOwner || !styleOwner) { + throw Error(`Missing placement or style property owner for uri: ${uri}`); + } + + return ( + <> + + + } + rightSection={''} + /> + + + + + Renderable + + + + Placement + + + + Style + + + + + + + + + + + + + + + + + ); +} From 704a0cbeb151776ea93f372f1d9e92a7369af069 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Mon, 10 Aug 2026 10:15:11 +0200 Subject: [PATCH 02/43] Make the add part a bit smaller --- .../en/panel-screenspacerenderable.json | 14 +++++----- .../ScreenSpaceRenderablePanel/ImageTab.tsx | 27 +++++++++++++------ .../ScreenSpaceRenderablePanel.tsx | 3 +-- .../ScreenSpaceRenderablePanel/WebpageTab.tsx | 27 +++++++++++++------ 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 8d646fe32..36bacfc2a 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -1,19 +1,21 @@ { "display-name-input": { - "title": "Display name", - "placeholder": "Slide name" + "title": "Name", + "placeholder": "Display name" }, "image-input": { "tab-title": "Image", - "title": "Path or URL to image", + "title": "Path / URL", "placeholder": "Path / URL", - "button-label": "Add" + "add-button-aria-label": "Add image", + "add-button-disabled-tooltip": "Enter a valid name and path/URL to add a screenspace image" }, "website-input": { "tab-title": "Website", - "title": "URL to website", + "title": "URL", "placeholder": "URL", - "button-label": "Add" + "add-button-aria-label": "Add website", + "add-button-disabled-tooltip": "Enter a valid name and URL to add a screenspace website" }, "added-slides": { "empty-slides": "No active slides", diff --git a/src/panels/ScreenSpaceRenderablePanel/ImageTab.tsx b/src/panels/ScreenSpaceRenderablePanel/ImageTab.tsx index affa25c97..3f75a854c 100644 --- a/src/panels/ScreenSpaceRenderablePanel/ImageTab.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/ImageTab.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Group, TextInput } from '@mantine/core'; +import { ActionIcon, Group, TextInput } from '@mantine/core'; import { useOpenSpaceApi } from '@/api/hooks'; +import { MaybeTooltip } from '@/components/MaybeTooltip/MaybeTooltip'; import { AddPhotoIcon } from '@/icons/icons'; import { useAppDispatch } from '@/redux/hooks'; import { handleNotificationLogging } from '@/redux/logging/loggingMiddleware'; @@ -81,26 +82,36 @@ export function ImageTab() { } return ( - + setSlideName(event.currentTarget.value)} placeholder={t('display-name-input.placeholder')} label={t('display-name-input.title')} + flex={1} /> setSlideURL(event.currentTarget.value)} placeholder={t('image-input.placeholder')} label={t('image-input.title')} + flex={1} /> - + + + + ); } diff --git a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx index 6dc179479..44606b2c3 100644 --- a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx @@ -35,7 +35,6 @@ export function ScreenSpaceRenderablePanel() { {t('website-input.tab-title')} - @@ -52,7 +51,7 @@ export function ScreenSpaceRenderablePanel() { {t('added-slides.empty-slides')} ) : ( <> - + {renderables.map((uri) => ( + setSlideName(event.currentTarget.value)} placeholder={t('display-name-input.placeholder')} label={t('display-name-input.title')} + flex={1} /> setSlideURL(event.currentTarget.value)} placeholder={t('website-input.placeholder')} label={t('website-input.title')} + flex={1} /> - + + + + ); } From f3e04cbdb9e8d2d358680e1e56ec4d4065d64db6 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Mon, 10 Aug 2026 12:08:51 +0200 Subject: [PATCH 03/43] Make decorated icon more compact, by overlaying a theme icon Also create a separate component to be used for building add icons with a consistent look --- .../DecoratedIcon/DecoratedAddIcon.tsx | 21 ++++ .../DecoratedIcon/DecoratedIcon.tsx | 114 ++++++++++++------ .../CapabilityEntry.tsx | 10 +- .../GlobeImageryBrowserPanel.tsx | 8 +- 4 files changed, 103 insertions(+), 50 deletions(-) create mode 100644 src/components/DecoratedIcon/DecoratedAddIcon.tsx diff --git a/src/components/DecoratedIcon/DecoratedAddIcon.tsx b/src/components/DecoratedIcon/DecoratedAddIcon.tsx new file mode 100644 index 000000000..db8298270 --- /dev/null +++ b/src/components/DecoratedIcon/DecoratedAddIcon.tsx @@ -0,0 +1,21 @@ +import { MantineSize } from '@mantine/core'; + +import { DecoratedIcon } from '@/components/DecoratedIcon/DecoratedIcon'; +import { PlusIcon } from '@/icons/icons'; + +interface Props { + baseIcon: React.ReactNode; + size?: MantineSize; +} + +/** + * Component used to create a consistent look for icons that represents the action of + * adding something + */ +export function DecoratedAddIcon({ baseIcon, size }: Props) { + return ( + }> + {baseIcon} + + ); +} diff --git a/src/components/DecoratedIcon/DecoratedIcon.tsx b/src/components/DecoratedIcon/DecoratedIcon.tsx index 4665c1e26..c8c1cf1b4 100644 --- a/src/components/DecoratedIcon/DecoratedIcon.tsx +++ b/src/components/DecoratedIcon/DecoratedIcon.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from 'react'; -import { Box, MantineSize } from '@mantine/core'; +import { Box, MantineSize, ThemeIcon, ThemeIconVariant } from '@mantine/core'; import { PlusIcon } from '@/icons/icons'; import { IconSize } from '@/types/enums'; @@ -7,10 +7,30 @@ import { IconSize } from '@/types/enums'; type DecoratorPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; interface Props extends PropsWithChildren { - renderDecorator?: (size: number) => React.JSX.Element; + /** + * The icon to be used as the decorator. If not provided, a default plus icon will be used + */ + decoratorIcon?: React.ReactNode; + + /** + * The variant of the decorator icon. Defaults to 'default' + */ + decoratorVariant?: ThemeIconVariant; + + /** + * The position of the decorator icon relative to the base icon. Defaults to 'top-right' + */ position?: DecoratorPosition; + + /** + * The size of the base icon. Defaults to 'xs' + */ size?: MantineSize; - offset?: { x: number; y: number }; + + /** + * An optional offset that may be used to adjust the position of the decorator icon + */ + offset?: { x?: number; y?: number }; } const MappedWrapperSize: Record = { @@ -21,57 +41,77 @@ const MappedWrapperSize: Record = { xl: IconSize.xl }; -const MappedDecoratorPosition: Record = { - xs: { x: 6, y: 10 }, - sm: { x: 6, y: 10 }, - md: { x: 6, y: 8 }, - lg: { x: 6, y: 6 }, - xl: { x: 6, y: 4 } -}; - const MappedDecoratorSize: Record = { - xs: 9, + xs: 8, sm: 9, md: 10, - lg: 10, - xl: 12 + lg: 12, + xl: 14 }; +/** + * Decorates an icon with a smaller icon in one of the corners. The decorator icon can be + * customized, but defaults to a plus icon. + */ export function DecoratedIcon({ children, - renderDecorator, + decoratorIcon, + decoratorVariant = 'default', position = 'top-right', size = 'xs', offset = { x: 0, y: 0 } }: Props) { const wrapperSize = MappedWrapperSize[size]; - const { x: offsetX, y: offsetY } = MappedDecoratorPosition[size]; + const decoratorSize = MappedDecoratorSize[size]; + const halfSize = wrapperSize / 2; + + const offsetX = offset.x ?? 0; + const offsetY = offset.y ?? 0; + + // Place the decorator at each corner, offset to overlap by a certain amount based off + // of the decorator size + const overlapX = 0.3 * decoratorSize; + const overlapY = 0.5 * decoratorSize; + const overlapYBottom = 0.4 * decoratorSize; + + function buildAbsolutePosition(x: number, y: number) { + return { + top: '50%', + left: '50%', + transform: `translate(-50%, -50%) translate(${x}px, ${y}px)` + }; + } - const mappedDecoratorPositionCSS: Record = { - 'top-left': { top: -offsetY + offset.y, left: -offsetX + offset.x }, - 'top-right': { top: -offsetY + offset.y, right: -offsetX + offset.x }, - 'bottom-left': { bottom: -offsetY + offset.y, left: -offsetX + offset.x }, - 'bottom-right': { bottom: -offsetY + offset.y, right: -offsetX + offset.x } + const MappedDecoratorPositionCSS: Record = { + 'top-left': buildAbsolutePosition( + -halfSize + overlapX + offsetX, + -halfSize + overlapY + offsetY + ), + 'top-right': buildAbsolutePosition( + halfSize - overlapX + offsetX, + -halfSize + overlapY + offsetY + ), + 'bottom-left': buildAbsolutePosition( + -halfSize + overlapX + offsetX, + halfSize - overlapYBottom + offsetY + ), + 'bottom-right': buildAbsolutePosition( + halfSize - overlapX + offsetX, + halfSize - overlapYBottom + offsetY + ) }; return ( - + {children} - - {renderDecorator ? ( - renderDecorator(MappedDecoratorSize[size]) - ) : ( - - )} + + + {decoratorIcon || } + ); diff --git a/src/panels/GlobeImageryBrowserPanel/CapabilityEntry.tsx b/src/panels/GlobeImageryBrowserPanel/CapabilityEntry.tsx index 621ae85c8..9ae634b55 100644 --- a/src/panels/GlobeImageryBrowserPanel/CapabilityEntry.tsx +++ b/src/panels/GlobeImageryBrowserPanel/CapabilityEntry.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { ActionIcon, Button, Group, Menu, Stack, Tooltip } from '@mantine/core'; -import { DecoratedIcon } from '@/components/DecoratedIcon/DecoratedIcon'; +import { DecoratedAddIcon } from '@/components/DecoratedIcon/DecoratedAddIcon'; import { MaybeTooltip } from '@/components/MaybeTooltip/MaybeTooltip'; import { TruncatedText } from '@/components/TruncatedText/TruncatedText'; import { layerGroups } from '@/data/GlobeLayers'; @@ -70,9 +70,7 @@ export const CapabilityEntry = memo( disabled={isInLayerGroup('ColorLayers')} aria-label={t('aria-labels.add-color-layer', { layerName: capability.Name })} > - - - + } /> @@ -93,9 +91,7 @@ export const CapabilityEntry = memo( + + + {t('title')} + + } + centered + > + + setSlideName(event.currentTarget.value)} + placeholder={t('name-input.placeholder')} + label={t('name-input.title')} + required + /> + + + }> + {t('image.tab-title')} + + }> + {t('website.tab-title')} + + + + + setSlideUrl(event.currentTarget.value)} + placeholder={t('image.placeholder')} + label={t('image.title')} + flex={1} + required + /> + + + + + setSlideUrl(event.currentTarget.value)} + placeholder={t('website.placeholder')} + label={t('website.title')} + flex={1} + required + /> + + + + + + + + + + + + ); +} diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts b/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts new file mode 100644 index 000000000..0792aad92 --- /dev/null +++ b/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts @@ -0,0 +1,96 @@ +import { useTranslation } from 'react-i18next'; + +import { useOpenSpaceApi } from '@/api/hooks'; +import { useAppDispatch } from '@/redux/hooks'; +import { handleNotificationLogging } from '@/redux/logging/loggingMiddleware'; +import { NotificationLevel } from '@/types/enums'; +import { Identifier } from '@/types/types'; + +interface ScreenSpaceRenderable { + Identifier: Identifier; + Name: string; +} + +interface ScreenSpaceImage extends ScreenSpaceRenderable { + Type: 'ScreenSpaceImageLocal' | 'ScreenSpaceImageOnline'; + TexturePath?: string; + URL?: string; +} + +interface ScreenSpaceBrowser extends ScreenSpaceRenderable { + Type: 'ScreenSpaceBrowser'; + Url: string; +} + +export function useAddScreenSpaceRenderable() { + const { t } = useTranslation('panel-screenspacerenderable', { keyPrefix: 'add-modal' }); + + const luaApi = useOpenSpaceApi(); + const dispatch = useAppDispatch(); + + async function addImage(name: string, slideURL: string) { + const osIdentifier = (await luaApi?.makeIdentifier(name)) ?? name; + + const renderable: ScreenSpaceImage = { + Identifier: osIdentifier, + Name: name, + Type: 'ScreenSpaceImageLocal' + }; + + let urlOrPath = slideURL; + if (slideURL.startsWith('data:image/')) { + let url = slideURL; + // Someone tried to paste a base64 encoded image. It starts with the text: + // data:image/{png/jpeg};base, + // followed by the rest of the image data in base64 encoding + url = url.substring('data:image/'.length); + + const filetype = url.substring(0, url.indexOf(';')); + if (filetype !== 'png' && filetype !== 'jpeg') { + dispatch( + handleNotificationLogging( + t('image.error.title'), + t('image.error.description', { format: filetype }), + NotificationLevel.Error + ) + ); + return; + } + + // Remove the remaining header information, at which point it becomes the data + const data = url.substring(url.indexOf(',') + 1); + + // eslint-disable-next-line no-template-curly-in-string + const tempPath = await luaApi?.absPath('${TEMPORARY}'); + const localPath = `${tempPath}/screenspace-slide-${name}.${filetype}`; + await luaApi?.saveBase64File(localPath, data); + urlOrPath = localPath; + } + + const isHttpSlide = urlOrPath.indexOf('http') === 0; + if (isHttpSlide) { + renderable.Type = 'ScreenSpaceImageOnline'; + renderable.URL = urlOrPath; + } else { + renderable.Type = 'ScreenSpaceImageLocal'; + renderable.TexturePath = urlOrPath; + } + + luaApi?.addScreenSpaceRenderable(renderable); + } + + async function addWebpage(name: string, slideURL: string) { + const osIdentifier = (await luaApi?.makeIdentifier(name)) ?? name; + + const renderable: ScreenSpaceBrowser = { + Identifier: osIdentifier, + Name: name, + Type: 'ScreenSpaceBrowser', + Url: slideURL + }; + + luaApi?.addScreenSpaceRenderable(renderable); + } + + return { addImage, addWebpage }; +} diff --git a/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddImageTab.tsx b/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddImageTab.tsx deleted file mode 100644 index 2e9b17484..000000000 --- a/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddImageTab.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ActionIcon, Group, TextInput } from '@mantine/core'; - -import { useOpenSpaceApi } from '@/api/hooks'; -import { DecoratedAddIcon } from '@/components/DecoratedIcon/DecoratedAddIcon'; -import { MaybeTooltip } from '@/components/MaybeTooltip/MaybeTooltip'; -import { InsertPhotoIcon } from '@/icons/icons'; -import { useAppDispatch } from '@/redux/hooks'; -import { handleNotificationLogging } from '@/redux/logging/loggingMiddleware'; -import { IconSize, NotificationLevel } from '@/types/enums'; -import { Identifier } from '@/types/types'; - -interface ScreenSpaceRenderable { - Identifier: Identifier; - Name: string; - Type: 'ScreenSpaceImageLocal' | 'ScreenSpaceImageOnline'; - TexturePath?: string; - URL?: string; -} - -export function AddImageTab() { - const { t } = useTranslation('panel-screenspacerenderable'); - - const [slideName, setSlideName] = useState(''); - const [slideURL, setSlideURL] = useState(''); - const luaApi = useOpenSpaceApi(); - const dispatch = useAppDispatch(); - - const isButtonDisabled = !slideName || !slideURL; - - async function addSlide() { - const osIdentifier = (await luaApi?.makeIdentifier(slideName)) ?? slideName; - - const renderable: ScreenSpaceRenderable = { - Identifier: osIdentifier, - Name: slideName, - Type: 'ScreenSpaceImageLocal' - }; - - let urlOrPath = slideURL; - if (slideURL.startsWith('data:image/')) { - let url = slideURL; - // Someone tried to paste a base64 encoded image. It starts with the text: - // data:image/{png/jpeg};base, - // followed by the rest of the image data in base64 encoding - url = url.substring('data:image/'.length); - - const filetype = url.substring(0, url.indexOf(';')); - if (filetype !== 'png' && filetype !== 'jpeg') { - dispatch( - handleNotificationLogging( - t('error.title'), - t('error.description', { format: filetype }), - NotificationLevel.Error - ) - ); - return; - } - - // Remove the remaining header information, at which point it becomes the data - const data = url.substring(url.indexOf(',') + 1); - - // eslint-disable-next-line no-template-curly-in-string - const tempPath = await luaApi?.absPath('${TEMPORARY}'); - const localPath = `${tempPath}/screenspace-slide-${slideName}.${filetype}`; - await luaApi?.saveBase64File(localPath, data); - urlOrPath = localPath; - } - - const isHttpSlide = urlOrPath.indexOf('http') === 0; - if (isHttpSlide) { - renderable.Type = 'ScreenSpaceImageOnline'; - renderable.URL = urlOrPath; - } else { - renderable.Type = 'ScreenSpaceImageLocal'; - renderable.TexturePath = urlOrPath; - } - - luaApi?.addScreenSpaceRenderable(renderable); - setSlideName(''); - setSlideURL(''); - } - - return ( - - setSlideName(event.currentTarget.value)} - placeholder={t('display-name-input.placeholder')} - label={t('display-name-input.title')} - flex={1} - /> - setSlideURL(event.currentTarget.value)} - placeholder={t('image-input.placeholder')} - label={t('image-input.title')} - flex={1} - /> - - - } - /> - - - - ); -} diff --git a/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddWebpageTab.tsx b/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddWebpageTab.tsx deleted file mode 100644 index d4021b546..000000000 --- a/src/panels/ScreenSpaceRenderablePanel/AddTabs/AddWebpageTab.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ActionIcon, Group, TextInput } from '@mantine/core'; - -import { useOpenSpaceApi } from '@/api/hooks'; -import { DecoratedAddIcon } from '@/components/DecoratedIcon/DecoratedAddIcon'; -import { MaybeTooltip } from '@/components/MaybeTooltip/MaybeTooltip'; -import { WebIcon } from '@/icons/icons'; -import { IconSize } from '@/types/enums'; -import { Identifier } from '@/types/types'; - -interface ScreenSpaceBrowser { - Identifier: Identifier; - Name: string; - Type: 'ScreenSpaceBrowser'; - Url: string; -} - -export function AddWebpageTab() { - const { t } = useTranslation('panel-screenspacerenderable'); - - const [slideName, setSlideName] = useState(''); - const [slideURL, setSlideURL] = useState(''); - const luaApi = useOpenSpaceApi(); - - const isAddButtonDisabled = !slideName || !slideURL; - - async function addSlide() { - const osIdentifier = (await luaApi?.makeIdentifier(slideName)) ?? slideName; - - const renderable: ScreenSpaceBrowser = { - Identifier: osIdentifier, - Name: slideName, - Type: 'ScreenSpaceBrowser', - Url: slideURL - }; - - luaApi?.addScreenSpaceRenderable(renderable); - setSlideName(''); - setSlideURL(''); - } - - return ( - - setSlideName(event.currentTarget.value)} - placeholder={t('display-name-input.placeholder')} - label={t('display-name-input.title')} - flex={1} - /> - setSlideURL(event.currentTarget.value)} - placeholder={t('website-input.placeholder')} - label={t('website-input.title')} - flex={1} - /> - - - } /> - - - - ); -} diff --git a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx index ae19faf1b..dd7925185 100644 --- a/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/ScreenSpaceRenderablePanel.tsx @@ -1,17 +1,14 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Divider, Tabs, Text } from '@mantine/core'; +import { Box, Divider, Group, Text, Title } from '@mantine/core'; -import { DecoratedAddIcon } from '@/components/DecoratedIcon/DecoratedAddIcon'; import { ResizeableContent } from '@/components/ResizeableContent/ResizeableContent'; import { ScrollBox } from '@/components/ScrollBox/ScrollBox'; import { usePropertyOwner } from '@/hooks/propertyOwner'; -import { InsertPhotoIcon, WebIcon } from '@/icons/icons'; import { Uri } from '@/types/types'; import { ScreenSpaceKey } from '@/util/keys'; -import { AddImageTab } from './AddTabs/AddImageTab'; -import { AddWebpageTab } from './AddTabs/AddWebpageTab'; +import { AddModal } from './Add/AddModal'; import { ScreenSpaceRenderableListItem } from './ScreenSpaceRenderableListItem'; import { ScreenSpaceRenderableView } from './ScreenSpaceRenderableView'; @@ -26,38 +23,17 @@ export function ScreenSpaceRenderablePanel() { return ( <> - - - } />} - > - {t('image-input.tab-title')} - - } />} - > - {t('website-input.tab-title')} - - - - - - - - - - - - + + {t('added-slides.title')} + + {renderables.length === 0 ? ( {t('added-slides.empty-slides')} ) : ( <> - + {renderables.map((uri) => ( Date: Wed, 12 Aug 2026 15:22:35 +0200 Subject: [PATCH 20/43] Add support for loading videos --- .../en/panel-screenspacerenderable.json | 9 +++++++-- .../Add/AddModal.tsx | 20 +++++++++++++++++-- .../ScreenSpaceRenderablePanel/Add/hooks.ts | 20 ++++++++++++++++++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 9b9c3fe3a..63c890eac 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -2,7 +2,7 @@ "add-modal": { "button-label": "Add new", - "title": "Add New ScreenSpace Renderable", + "title": "Add New Screenspace Renderable", "name-input": { "title": "Display name", "placeholder": "Enter a name to show in the GUI" @@ -21,13 +21,18 @@ "title": "Website URL", "placeholder": "https://example.com" }, + "video": { + "tab-title": "Video", + "title": "Path to video file", + "placeholder": "C:/path/to/video.mp4" + }, "add-button": { "label": "Add", "disabled-tooltip": "Please provide a valid name and location" } }, "added-slides": { - "title": "Added slides", + "title": "Slides", "empty-slides": "No active slides", "remove-slide-aria-label": "Remove slide" }, diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx index db0049625..dfc242fcc 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx @@ -9,6 +9,7 @@ import { InsertPhotoIcon, OpenInBrowserIcon, PlusIcon, + VideoIcon, WebIcon } from '@/icons/icons'; import { IconSize } from '@/types/enums'; @@ -24,7 +25,7 @@ export function AddModal() { const [opened, { open, close }] = useDisclosure(false); - const { addImage, addWebpage } = useAddScreenSpaceRenderable(); + const { addImage, addWebpage, addVideo } = useAddScreenSpaceRenderable(); const isAddButtonDisabled = !slideName || !slideUrl; @@ -43,6 +44,9 @@ export function AddModal() { case 'web': addWebpage(slideName, slideUrl); break; + case 'video': + addVideo(slideName, slideUrl); + break; default: throw new Error(`Unknown tab value: ${activeTab}`); } @@ -87,6 +91,9 @@ export function AddModal() { }> {t('website.tab-title')} + }> + {t('video.tab-title')} + @@ -95,7 +102,6 @@ export function AddModal() { onChange={(event) => setSlideUrl(event.currentTarget.value)} placeholder={t('image.placeholder')} label={t('image.title')} - flex={1} required /> @@ -120,6 +126,16 @@ export function AddModal() { + + + setSlideUrl(event.currentTarget.value)} + placeholder={t('video.placeholder')} + label={t('video.title')} + required + /> + Date: Wed, 12 Aug 2026 15:38:19 +0200 Subject: [PATCH 21/43] handle quotes in file name and add video settings for look and audio --- .../en/panel-screenspacerenderable.json | 8 +++- .../Add/AddModal.tsx | 40 ++++++++++++++++--- .../ScreenSpaceRenderablePanel/Add/hooks.ts | 12 +++++- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 63c890eac..5d364dc73 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -24,7 +24,13 @@ "video": { "tab-title": "Video", "title": "Path to video file", - "placeholder": "C:/path/to/video.mp4" + "placeholder": "C:/path/to/video.mp4", + "loop": { + "label": "Loop video" + }, + "play-audio": { + "label": "Play audio" + } }, "add-button": { "label": "Add", diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx index dfc242fcc..1a6c13514 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Button, Group, Modal, Stack, Tabs, TextInput } from '@mantine/core'; +import { Box, Button, Group, Modal, Stack, Switch, Tabs, TextInput } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { MaybeTooltip } from '@/components/MaybeTooltip/MaybeTooltip'; @@ -19,8 +19,13 @@ import { useAddScreenSpaceRenderable } from './hooks'; export function AddModal() { const { t } = useTranslation('panel-screenspacerenderable', { keyPrefix: 'add-modal' }); - const [slideName, setSlideName] = useState(''); - const [slideUrl, setSlideUrl] = useState(''); + const [slideName, setSlideName] = useState(''); + const [slideUrl, setSlideUrl] = useState(''); + + // Some video-specific options + const [shouldLoop, setShouldLoop] = useState(true); + const [playAudio, setPlayAudio] = useState(false); + const [activeTab, setActiveTab] = useState('images'); const [opened, { open, close }] = useDisclosure(false); @@ -37,15 +42,24 @@ export function AddModal() { } function onAdd() { + const removeSurroundingQuotes = (value: string) => + value.replace(/^(['"])(.*)\1$/, '$2'); + + const sanitizedName = removeSurroundingQuotes(slideName.trim()); + const sanitizedUrl = removeSurroundingQuotes(slideUrl.trim()); + switch (activeTab) { case 'images': - addImage(slideName, slideUrl); + addImage(sanitizedName, sanitizedUrl); break; case 'web': - addWebpage(slideName, slideUrl); + addWebpage(sanitizedName, sanitizedUrl); break; case 'video': - addVideo(slideName, slideUrl); + addVideo(sanitizedName, sanitizedUrl, { + shouldLoop: shouldLoop, + playAudio: playAudio + }); break; default: throw new Error(`Unknown tab value: ${activeTab}`); @@ -135,6 +149,20 @@ export function AddModal() { label={t('video.title')} required /> + + setShouldLoop(event.currentTarget.checked)} + mt={'xs'} + /> + setPlayAudio(event.currentTarget.checked)} + mt={'xs'} + /> + diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts b/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts index a16baa3ed..7a80cdb46 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts +++ b/src/panels/ScreenSpaceRenderablePanel/Add/hooks.ts @@ -25,6 +25,8 @@ interface ScreenSpaceBrowser extends ScreenSpaceRenderable { interface ScreenSpaceVideo extends ScreenSpaceRenderable { Type: 'ScreenSpaceVideo'; Video: string; // Path to video file + LoopVideo?: boolean; // Whether the video should loop + PlayAudio?: boolean; // Whether the video should play audio } export function useAddScreenSpaceRenderable() { @@ -97,14 +99,20 @@ export function useAddScreenSpaceRenderable() { luaApi?.addScreenSpaceRenderable(renderable); } - async function addVideo(name: string, slideURL: string) { + async function addVideo( + name: string, + slideURL: string, + { shouldLoop, playAudio }: { shouldLoop?: boolean; playAudio?: boolean } = {} + ) { const osIdentifier = (await luaApi?.makeIdentifier(name)) ?? name; const renderable: ScreenSpaceVideo = { Identifier: osIdentifier, Name: name, Type: 'ScreenSpaceVideo', - Video: slideURL + Video: slideURL, + LoopVideo: shouldLoop, + PlayAudio: playAudio }; luaApi?.addScreenSpaceRenderable(renderable); From 59e4153b5124f16ab668e2eeb11517e62f6876c5 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Wed, 12 Aug 2026 16:38:03 +0200 Subject: [PATCH 22/43] Add some more icons --- src/icons/icons.tsx | 4 +- .../ScreenSpaceRenderablePanel/TypeIcon.tsx | 49 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/icons/icons.tsx b/src/icons/icons.tsx index 20f48fffb..0614c2bab 100644 --- a/src/icons/icons.tsx +++ b/src/icons/icons.tsx @@ -36,7 +36,8 @@ export { IoMdGlobe as GlobeIcon, IoMdTime as TimeIcon } from 'react-icons/io'; export { IoInformationCircleOutline as InformationCircleOutlineIcon, IoInformation as InformationIcon, - IoTelescopeOutline as TelescopeIcon + IoTelescopeOutline as TelescopeIcon, + IoText as TextIcon } from 'react-icons/io5'; export { LuFilePlus2 as AddFileIcon, @@ -145,6 +146,7 @@ export { TbCube as SceneIcon, TbScript as ScriptLogIcon, TbServer as ServerIcon, + TbShape as ShapeIcon, TbWorldWww as WebIcon } from 'react-icons/tb'; export { diff --git a/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx b/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx index da734aa8f..a90a96466 100644 --- a/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx @@ -1,7 +1,17 @@ -import { MdOutlineNote } from 'react-icons/md'; import { Tooltip } from '@mantine/core'; -import { InsertPhotoIcon, SceneIcon, VideoIcon, WebIcon } from '@/icons/icons'; +import { + CalendarIcon, + InsertPhotoIcon, + SceneIcon, + ShapeIcon, + TelescopeIcon, + TextIcon, + TimeIcon, + VideoIcon, + WebIcon +} from '@/icons/icons'; +import { IconSize } from '@/types/enums'; interface Props { type: string | undefined; @@ -12,7 +22,7 @@ export function ScreenSpaceRenderableTypeIcon({ type, size }: Props) { switch (type) { case 'ScreenSpaceBrowser': return ( - + ); @@ -35,12 +45,37 @@ export function ScreenSpaceRenderableTypeIcon({ type, size }: Props) { ); - default: - // TODO: deciede on default icon for unknown types + case 'ScreenSpaceText': + return ( + + + + ); + case 'ScreenSpaceDate': + return ( + + + + ); + case 'ScreenSpaceSkyBrowser': return ( - - + + ); + case 'ScreenSpaceInsetBlackout': + return ( + + + + ); + case 'ScreenSpaceTimeVaryingImageOnline': + return ( + + + + ); + default: + return <>; } } From f2976a5ce8b59007a2844496d26483b694a74880 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Wed, 12 Aug 2026 16:54:55 +0200 Subject: [PATCH 23/43] Add option to add text --- .../en/panel-screenspacerenderable.json | 5 ++ .../Add/AddModal.tsx | 53 +++++++++++++------ .../ScreenSpaceRenderablePanel/Add/hooks.ts | 20 ++++++- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 5d364dc73..52f1c452f 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -32,6 +32,11 @@ "label": "Play audio" } }, + "text": { + "tab-title": "Text", + "title": "Text content", + "placeholder": "Enter the text to display" + }, "add-button": { "label": "Add", "disabled-tooltip": "Please provide a valid name and location" diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx index 1a6c13514..f04df5ffb 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx @@ -9,6 +9,7 @@ import { InsertPhotoIcon, OpenInBrowserIcon, PlusIcon, + TextIcon, VideoIcon, WebIcon } from '@/icons/icons'; @@ -20,7 +21,7 @@ export function AddModal() { const { t } = useTranslation('panel-screenspacerenderable', { keyPrefix: 'add-modal' }); const [slideName, setSlideName] = useState(''); - const [slideUrl, setSlideUrl] = useState(''); + const [slideData, setSlideData] = useState(''); // Some video-specific options const [shouldLoop, setShouldLoop] = useState(true); @@ -30,14 +31,14 @@ export function AddModal() { const [opened, { open, close }] = useDisclosure(false); - const { addImage, addWebpage, addVideo } = useAddScreenSpaceRenderable(); + const { addImage, addWebpage, addVideo, addText } = useAddScreenSpaceRenderable(); - const isAddButtonDisabled = !slideName || !slideUrl; + const isAddButtonDisabled = !slideName || !slideData; function onTabChange(value: string | null) { if (value) { setActiveTab(value); - setSlideUrl(''); + setSlideData(''); } } @@ -46,21 +47,24 @@ export function AddModal() { value.replace(/^(['"])(.*)\1$/, '$2'); const sanitizedName = removeSurroundingQuotes(slideName.trim()); - const sanitizedUrl = removeSurroundingQuotes(slideUrl.trim()); + const sanitizedData = removeSurroundingQuotes(slideData.trim()); switch (activeTab) { case 'images': - addImage(sanitizedName, sanitizedUrl); + addImage(sanitizedName, sanitizedData); break; case 'web': - addWebpage(sanitizedName, sanitizedUrl); + addWebpage(sanitizedName, sanitizedData); break; case 'video': - addVideo(sanitizedName, sanitizedUrl, { + addVideo(sanitizedName, sanitizedData, { shouldLoop: shouldLoop, playAudio: playAudio }); break; + case 'text': + addText(sanitizedName, sanitizedData); + break; default: throw new Error(`Unknown tab value: ${activeTab}`); } @@ -69,7 +73,7 @@ export function AddModal() { function onClose() { setSlideName(''); - setSlideUrl(''); + setSlideData(''); close(); } @@ -108,12 +112,15 @@ export function AddModal() { }> {t('video.tab-title')} + }> + {t('text.tab-title')} + setSlideUrl(event.currentTarget.value)} + value={slideData} + onChange={(event) => setSlideData(event.currentTarget.value)} placeholder={t('image.placeholder')} label={t('image.title')} required @@ -123,17 +130,19 @@ export function AddModal() { setSlideUrl(event.currentTarget.value)} + value={slideData} + onChange={(event) => setSlideData(event.currentTarget.value)} placeholder={t('website.placeholder')} label={t('website.title')} flex={1} required /> + + + From f6dc22fbea0d0e253a9c85f2d80d42246f4afe24 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Tue, 18 Aug 2026 16:27:09 +0200 Subject: [PATCH 37/43] Move texts to translation system --- .../en/panel-screenspacerenderable.json | 104 +++++++++++++++++- .../Placement/CartesianControls.tsx | 10 +- .../Placement/LocalRotationControls.tsx | 20 +++- .../RadiusAzimuthElevationControls.tsx | 21 ++-- .../Placement/ScreenSpacePlacementOwner.tsx | 11 +- .../ScreenSpaceRenderableView.tsx | 26 ++--- 6 files changed, 154 insertions(+), 38 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index a1610b1e6..3bc55206a 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -43,7 +43,7 @@ }, "add-button": { "label": "Add", - "disabled-tooltip": "Please provide a valid name and location" + "disabled-tooltip": "Please provide some valid content for the screenspace renderable." } }, "added-slides": { @@ -65,5 +65,107 @@ "cancel-button": "Cancel" }, "pop-out": "Pop out" + }, + "renderable-view": { + "placement-tab": { + "title": "Placement", + "mode-switch": { + "aria-label": "Placement mode", + "label-rae": "Spherical (RAE)", + "label-xyz": "Cartesian (XYZ)" + }, + "spherical-controls": { + "label": "Spherical position", + "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'", + "radius": { + "label": "Radius", + "aria-label": "Radius" + }, + "azimuth": { + "label": "Azimuth", + "aria-label": "Azimuth" + }, + "elevation": { + "label": "Elevation", + "aria-label": "Elevation" + } + }, + "cartesian-controls": { + "label": "Cartesian position", + "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'" + }, + "local-rotation": { + "label": "Local rotation", + "roll": { + "label": "Roll", + "aria-label": "Roll" + }, + "pitch": { + "label": "Pitch", + "aria-label": "Pitch" + }, + "yaw": { + "label": "Yaw", + "aria-label": "Yaw" + }, + "reset-button": { + "label": "Reset to all zeros", + "aria-label": "Reset local rotation to all zeros" + } + } + }, + "style-tab": { + "title": "Style" + }, + "renderable-tab": { + "title": "Renderable", + "tooltip": "Main properties of the screenspace renderable, including the ones for the specific type." + } + }, + "placement": { + "mode-switch": { + "aria-label": "Placement mode", + "label-rae": "Spherical (RAE)", + "label-xyz": "Cartesian (XYZ)" + }, + "spherical-controls": { + "label": "Spherical position", + "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'", + "radius": { + "label": "Radius", + "aria-label": "Radius" + }, + "azimuth": { + "label": "Azimuth", + "aria-label": "Azimuth" + }, + "elevation": { + "label": "Elevation", + "aria-label": "Elevation" + } + }, + "cartesian-controls": { + "label": "Cartesian position", + "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'" + }, + "local-rotation": { + "label": "Local rotation", + "roll": { + "label": "Roll", + "aria-label": "Roll" + }, + "pitch": { + "label": "Pitch", + "aria-label": "Pitch" + }, + "yaw": { + "label": "Yaw", + "aria-label": "Yaw" + }, + "reset-button": { + "label": "Reset to all zeros", + "aria-label": "Reset local rotation to all zeros" + } + } } } diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx index 4e2307633..ea414ea78 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next'; import { Group, Text } from '@mantine/core'; import { NumericInput } from '@/components/Input/NumericInput/NumericInput'; @@ -15,6 +16,9 @@ interface Props { } export function CartesianControls({ propertyUri }: Props) { + const { t } = useTranslation('panel-screenspacerenderable', { + keyPrefix: 'placement.cartesian-controls' + }); const [value, setValue, meta] = useProperty('Vec3Property', propertyUri); const isAdvancedUser = useIsAdvancedUserLevel(); @@ -27,10 +31,8 @@ export function CartesianControls({ propertyUri }: Props) { - - Renderable + + {t('renderable-tab.title')} - Placement + {t('placement-tab.title')} - Style + {t('style-tab.title')} - + From a20abebb01ec5c6cc4709b12d35f86220d4f7ce9 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Tue, 18 Aug 2026 16:32:02 +0200 Subject: [PATCH 38/43] Tiny cleanup --- public/locales/en/panel-screenspacerenderable.json | 4 ++-- src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 3bc55206a..766abd9d3 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -1,8 +1,8 @@ { "add-modal": { - "button-label": "Add new", - "title": "Add New Screenspace Renderable", + "button-label": "Add", + "title": "Add Screenspace Renderable", "name-input": { "title": "Display name", "placeholder": "Enter a name to show in the GUI" diff --git a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx index 36a4165bd..935747f01 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Add/AddModal.tsx @@ -111,7 +111,7 @@ export function AddModal() { return ( <> - From c2600aa089448a51ac08b536e7929f6eb030570c Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Tue, 18 Aug 2026 16:58:30 +0200 Subject: [PATCH 39/43] Small refactor --- .../en/panel-screenspacerenderable.json | 1 - .../ScreenSpaceRenderablePanel/TypeIcon.tsx | 96 +++++++------------ 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 766abd9d3..f7e9d64b0 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -1,5 +1,4 @@ { - "add-modal": { "button-label": "Add", "title": "Add Screenspace Renderable", diff --git a/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx b/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx index d77a960e9..aff03fe45 100644 --- a/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/TypeIcon.tsx @@ -18,70 +18,38 @@ interface Props { size?: number; } +const TypeConfig = { + ScreenSpaceBrowser: { label: 'Web page', Icon: WebIcon }, + ScreenSpaceImageLocal: { label: 'Image', Icon: InsertPhotoIcon }, + ScreenSpaceImageOnline: { label: 'Image', Icon: InsertPhotoIcon }, + ScreenSpaceVideo: { label: 'Video', Icon: VideoIcon }, + ScreenSpaceRenderableRenderable: { label: 'Renderable', Icon: SceneIcon }, + ScreenSpaceText: { label: 'Text', Icon: TextIcon }, + ScreenSpaceDate: { label: 'Date', Icon: CalendarIcon }, + ScreenSpaceSkyBrowser: { label: 'SkyBrowser', Icon: TelescopeIcon }, + ScreenSpaceInsetBlackout: { label: 'Blackout inset', Icon: ShapeIcon }, + ScreenSpaceTimeVaryingImageOnline: { + label: 'Time-varying image', + Icon: FileClockIcon + }, + ScreenSpaceDashboard: { label: 'Dashboard', Icon: TextShortIcon } +} as const; + export function ScreenSpaceRenderableTypeIcon({ type, size }: Props) { - switch (type) { - case 'ScreenSpaceBrowser': - return ( - - - - ); - case 'ScreenSpaceImageLocal': - case 'ScreenSpaceImageOnline': - return ( - - - - ); - case 'ScreenSpaceVideo': - return ( - - - - ); - case 'ScreenSpaceRenderableRenderable': - return ( - - - - ); - case 'ScreenSpaceText': - return ( - - - - ); - case 'ScreenSpaceDate': - return ( - - - - ); - case 'ScreenSpaceSkyBrowser': - return ( - - - - ); - case 'ScreenSpaceInsetBlackout': - return ( - - - - ); - case 'ScreenSpaceTimeVaryingImageOnline': - return ( - - - - ); - case 'ScreenSpaceDashboard': - return ( - - - - ); - default: - return <>; + if (!type) { + return <>; + } + + const config = TypeConfig[type as keyof typeof TypeConfig]; + if (!config) { + return <>; } + + const { label, Icon } = config; + + return ( + + + + ); } From 27d7c27ff05444dbae38793e768e9988dd203419 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Tue, 18 Aug 2026 17:12:08 +0200 Subject: [PATCH 40/43] Oops, fix duplicated translations --- .../en/panel-screenspacerenderable.json | 50 +------------------ .../Placement/CartesianControls.tsx | 2 +- .../Placement/LocalRotationControls.tsx | 2 +- .../RadiusAzimuthElevationControls.tsx | 2 +- .../Placement/ScreenSpacePlacementOwner.tsx | 2 +- 5 files changed, 6 insertions(+), 52 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index f7e9d64b0..868d13d8a 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -75,7 +75,7 @@ }, "spherical-controls": { "label": "Spherical position", - "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'", + "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.", "radius": { "label": "Radius", "aria-label": "Radius" @@ -91,7 +91,7 @@ }, "cartesian-controls": { "label": "Cartesian position", - "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'" + "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths." }, "local-rotation": { "label": "Local rotation", @@ -120,51 +120,5 @@ "title": "Renderable", "tooltip": "Main properties of the screenspace renderable, including the ones for the specific type." } - }, - "placement": { - "mode-switch": { - "aria-label": "Placement mode", - "label-rae": "Spherical (RAE)", - "label-xyz": "Cartesian (XYZ)" - }, - "spherical-controls": { - "label": "Spherical position", - "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'", - "radius": { - "label": "Radius", - "aria-label": "Radius" - }, - "azimuth": { - "label": "Azimuth", - "aria-label": "Azimuth" - }, - "elevation": { - "label": "Elevation", - "aria-label": "Elevation" - } - }, - "cartesian-controls": { - "label": "Cartesian position", - "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.'" - }, - "local-rotation": { - "label": "Local rotation", - "roll": { - "label": "Roll", - "aria-label": "Roll" - }, - "pitch": { - "label": "Pitch", - "aria-label": "Pitch" - }, - "yaw": { - "label": "Yaw", - "aria-label": "Yaw" - }, - "reset-button": { - "label": "Reset to all zeros", - "aria-label": "Reset local rotation to all zeros" - } - } } } diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx index ea414ea78..54d31aebc 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx @@ -17,7 +17,7 @@ interface Props { export function CartesianControls({ propertyUri }: Props) { const { t } = useTranslation('panel-screenspacerenderable', { - keyPrefix: 'placement.cartesian-controls' + keyPrefix: 'renderable-view.placement-tab.cartesian-controls' }); const [value, setValue, meta] = useProperty('Vec3Property', propertyUri); diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx index a372cea5a..e9bec7b34 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx @@ -20,7 +20,7 @@ interface Props { export function LocalRotationControls({ propertyUri }: Props) { const { t } = useTranslation('panel-screenspacerenderable', { - keyPrefix: 'placement.local-rotation' + keyPrefix: 'renderable-view.placement-tab.local-rotation' }); const [value, setValue, meta] = useProperty('Vec3Property', propertyUri); diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx index f362ceb85..5338f2904 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx @@ -18,7 +18,7 @@ interface Props { export function RadiusAzimuthElevationControls({ propertyUri }: Props) { const { t } = useTranslation('panel-screenspacerenderable', { - keyPrefix: 'placement.spherical-controls' + keyPrefix: 'renderable-view.placement-tab.spherical-controls' }); const [value, setValue, meta] = useProperty('Vec3Property', propertyUri); diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/ScreenSpacePlacementOwner.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/ScreenSpacePlacementOwner.tsx index 91136966d..ccafda158 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/ScreenSpacePlacementOwner.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/ScreenSpacePlacementOwner.tsx @@ -16,7 +16,7 @@ interface Props { export function ScreenSpacePlacementOwner({ uri }: Props) { const { t } = useTranslation('panel-screenspacerenderable', { - keyPrefix: 'placement' + keyPrefix: 'renderable-view.placement-tab' }); const propertyOwner = usePropertyOwner(uri); From 76f5544da15bdfd947ab38fb06d2265b47e16132 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Wed, 19 Aug 2026 09:00:11 +0200 Subject: [PATCH 41/43] Apply suggestions from code review Co-authored-by: Alexander Bock --- .../en/panel-screenspacerenderable.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 868d13d8a..9ad60221f 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -42,24 +42,24 @@ }, "add-button": { "label": "Add", - "disabled-tooltip": "Please provide some valid content for the screenspace renderable." + "disabled-tooltip": "Please provide some valid content for the Screenspace renderable." } }, "added-slides": { - "search-placeholder": "Search for added renderable...", - "empty": "Add screenspace renderables to view them here.", - "tips": "Tip: Try adding by drag-and-dropping an image or video file directly into OpenSpace." + "search-placeholder": "Search...", + "empty": "Add Screenspace renderables to view them here.", + "tips": "Tip: Try adding an image or video file by dragging and dropping it." }, "no-selection-hint": "Select an item to view its details", "more-menu": { "aria-label": "Open more menu", "delete-button": { "label": "Delete", - "info": "Remove this screenspace renderable" + "info": "Remove this Screenspace renderable" }, "delete-confirm-modal": { "title": "Delete Screenspace Renderable", - "are-you-sure": "Are you sure you want to remove the screenspace renderable", + "are-you-sure": "Are you sure you want to remove the Screenspace renderable", "remove-button": "Remove", "cancel-button": "Cancel" }, @@ -75,7 +75,7 @@ }, "spherical-controls": { "label": "Spherical position", - "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.", + "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for other user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.", "radius": { "label": "Radius", "aria-label": "Radius" @@ -91,7 +91,7 @@ }, "cartesian-controls": { "label": "Cartesian position", - "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for lower user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths." + "description": "Screenspace position in Cartesian coordinates (x, y, z). The z-coordinate is an advanced property hidden for other user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths." }, "local-rotation": { "label": "Local rotation", @@ -108,8 +108,8 @@ "aria-label": "Yaw" }, "reset-button": { - "label": "Reset to all zeros", - "aria-label": "Reset local rotation to all zeros" + "label": "Reset", + "aria-label": "Reset local rotation" } } }, @@ -118,7 +118,7 @@ }, "renderable-tab": { "title": "Renderable", - "tooltip": "Main properties of the screenspace renderable, including the ones for the specific type." + "tooltip": "Main properties of the Screenspace renderable, including the ones for the specific type." } } } From b6cafe2c5f53ba1043d9515fa7c2e27a3415865f Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Wed, 19 Aug 2026 09:01:08 +0200 Subject: [PATCH 42/43] Clarify that text is a tooltip --- public/locales/en/panel-screenspacerenderable.json | 2 +- .../Placement/LocalRotationControls.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index 9ad60221f..e24c3e3a1 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -108,7 +108,7 @@ "aria-label": "Yaw" }, "reset-button": { - "label": "Reset", + "tooltip": "Reset", "aria-label": "Reset local rotation" } } diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx index e9bec7b34..1f1deaf57 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/LocalRotationControls.tsx @@ -75,7 +75,7 @@ export function LocalRotationControls({ propertyUri }: Props) { } /> - + Date: Wed, 19 Aug 2026 09:24:25 +0200 Subject: [PATCH 43/43] Refactor aria labels --- .../en/panel-screenspacerenderable.json | 18 +++++---------- .../AngleInput}/AngleInput.tsx | 15 ++++++------ .../Placement/CartesianControls.tsx | 23 ++++++++++++++++--- .../Placement/LocalRotationControls.tsx | 6 ++--- .../RadiusAzimuthElevationControls.tsx | 14 +++++++---- 5 files changed, 45 insertions(+), 31 deletions(-) rename src/{panels/ScreenSpaceRenderablePanel/Placement => components/AngleInput}/AngleInput.tsx (89%) diff --git a/public/locales/en/panel-screenspacerenderable.json b/public/locales/en/panel-screenspacerenderable.json index e24c3e3a1..4fb7118eb 100644 --- a/public/locales/en/panel-screenspacerenderable.json +++ b/public/locales/en/panel-screenspacerenderable.json @@ -77,16 +77,13 @@ "label": "Spherical position", "description": "Screenspace position in spherical coordinates (radius, azimuth, elevation). Radius is an advanced property hidden for other user levels. It controls the distance to the screenspace plane, allowing objects to be layered at different depths.", "radius": { - "label": "Radius", - "aria-label": "Radius" + "label": "Radius" }, "azimuth": { - "label": "Azimuth", - "aria-label": "Azimuth" + "label": "Azimuth" }, "elevation": { - "label": "Elevation", - "aria-label": "Elevation" + "label": "Elevation" } }, "cartesian-controls": { @@ -96,16 +93,13 @@ "local-rotation": { "label": "Local rotation", "roll": { - "label": "Roll", - "aria-label": "Roll" + "label": "Roll" }, "pitch": { - "label": "Pitch", - "aria-label": "Pitch" + "label": "Pitch" }, "yaw": { - "label": "Yaw", - "aria-label": "Yaw" + "label": "Yaw" }, "reset-button": { "tooltip": "Reset", diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/AngleInput.tsx b/src/components/AngleInput/AngleInput.tsx similarity index 89% rename from src/panels/ScreenSpaceRenderablePanel/Placement/AngleInput.tsx rename to src/components/AngleInput/AngleInput.tsx index b6dad7cc1..2252d4036 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/AngleInput.tsx +++ b/src/components/AngleInput/AngleInput.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useId, useState } from 'react'; import { AngleSlider, Group, Stack } from '@mantine/core'; import { useThrottledCallback } from '@mantine/hooks'; @@ -6,19 +6,20 @@ import { NumericInput } from '@/components/Input/NumericInput/NumericInput'; interface Props { value: number; // Radians + label: React.ReactNode; onChange?: (value: number) => void; // Radians disabled?: boolean; - label?: React.ReactNode; - ariaLabel?: string; } -export function AngleInput({ value, onChange, disabled, label, ariaLabel }: Props) { +export function AngleInput({ value, label, onChange, disabled }: Props) { const [currentAngle, setCurrentAngle] = useState(radiansToDegrees(value)); const [isInteracting, setIsInteracting] = useState(false); const throttledNumberInputChange = useThrottledCallback((numericValue: number) => { onChange?.(degreesToRadians(numericValue)); }, 300); + const labelId = useId(); + function radiansToDegrees(radians: number) { return (radians * 180) / Math.PI; } @@ -35,7 +36,7 @@ export function AngleInput({ value, onChange, disabled, label, ariaLabel }: Prop return ( - {label} +
{label}
setIsInteracting(false)} formatLabel={(labelValue) => `${Math.round(labelValue)}°`} disabled={disabled} - aria-label={ariaLabel} + aria-labelledby={labelId} /> setIsInteracting(true)} onBlur={() => setIsInteracting(false)} disabled={disabled} - aria-label={ariaLabel} + aria-labelledby={labelId} />
diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx index 54d31aebc..ea278a933 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/CartesianControls.tsx @@ -1,3 +1,4 @@ +import { useId } from 'react'; import { useTranslation } from 'react-i18next'; import { Group, Text } from '@mantine/core'; @@ -23,6 +24,10 @@ export function CartesianControls({ propertyUri }: Props) { const isAdvancedUser = useIsAdvancedUserLevel(); + const xLabelId = useId(); + const yLabelId = useId(); + const zLabelId = useId(); + if (!value || !meta) { throw Error(`Missing property with uri: ${propertyUri}`); } @@ -36,12 +41,15 @@ export function CartesianControls({ propertyUri }: Props) { mt={'xs'} > - x + + x + - y + + y + {isAdvancedUser && ( - z + + z + setValue([Number(newValue), value[1], value[2]])} disabled={meta.isReadOnly} - ariaLabel={t('roll.aria-label')} label={ @@ -53,7 +53,6 @@ export function LocalRotationControls({ propertyUri }: Props) { value={value[1]} onChange={(newValue) => setValue([value[0], Number(newValue), value[2]])} disabled={meta.isReadOnly} - ariaLabel={t('pitch.aria-label')} label={ @@ -66,7 +65,6 @@ export function LocalRotationControls({ propertyUri }: Props) { value={value[2]} onChange={(newValue) => setValue([value[0], value[1], Number(newValue)])} disabled={meta.isReadOnly} - ariaLabel={t('yaw.aria-label')} label={ diff --git a/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx b/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx index 5338f2904..ffc6df1ee 100644 --- a/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx +++ b/src/panels/ScreenSpaceRenderablePanel/Placement/RadiusAzimuthElevationControls.tsx @@ -1,3 +1,4 @@ +import { useId } from 'react'; import { useTranslation } from 'react-i18next'; import { Group, Text } from '@mantine/core'; @@ -9,7 +10,8 @@ import { ArrowsLeftRightIcon, ArrowsUpDownIcon } from '@/icons/icons'; import { IconSize } from '@/types/enums'; import { Uri } from '@/types/types'; -import { AngleInput } from './AngleInput'; +import { AngleInput } from '../../../components/AngleInput/AngleInput'; + import { PropertyGroupContainer } from './PropertyGroupContainer'; interface Props { @@ -24,6 +26,7 @@ export function RadiusAzimuthElevationControls({ propertyUri }: Props) { const [value, setValue, meta] = useProperty('Vec3Property', propertyUri); const isAdvancedUser = useIsAdvancedUserLevel(); + const radiusLabelId = useId(); if (!value || !meta) { throw Error(`Missing property with uri: ${propertyUri}`); @@ -42,7 +45,6 @@ export function RadiusAzimuthElevationControls({ propertyUri }: Props) { value={value[1]} onChange={(newValue) => setValue([value[0], Number(newValue), value[2]])} disabled={meta.isReadOnly} - ariaLabel={t('azimuth.aria-label')} label={ @@ -55,7 +57,6 @@ export function RadiusAzimuthElevationControls({ propertyUri }: Props) { value={value[2]} onChange={(newValue) => setValue([value[0], value[1], Number(newValue)])} disabled={meta.isReadOnly} - ariaLabel={t('elevation.aria-label')} label={ @@ -66,9 +67,10 @@ export function RadiusAzimuthElevationControls({ propertyUri }: Props) { {isAdvancedUser && ( - {t('radius.label')} + + {t('radius.label')} + setValue([Number(newValue), value[1], value[2]])} + aria-labelledby={radiusLabelId} /> setValue([Number(newValue), value[1], value[2]])} + aria-labelledby={radiusLabelId} /> )}