Merge branch 'feature/initial-viewer-ui-updates' of https://github.com/specklesystems/speckle-server into feature/initial-viewer-ui-updates

This commit is contained in:
andrewwallacespeckle
2025-07-31 11:31:29 +02:00
8 changed files with 63 additions and 30 deletions
@@ -56,7 +56,7 @@
<template v-if="showControls">
<ViewerControlsLeft />
<ViewerControlsBottom />
<ViewerControlsRight v-if="!isMobile" />
<ViewerControlsRight v-if="isMobile" />
</template>
<ViewerLimitsDialog
@@ -19,7 +19,11 @@
hide-text
color="subtle"
:icon-left="settingsIcon"
:class="showVisibilityOptions ? '!text-primary-focus !bg-info-lighter' : ''"
:class="
showVisibilityOptions
? '!text-primary-focus !dark:text-foreground-on-primary !bg-info-lighter'
: ''
"
@click="showVisibilityOptions = !showVisibilityOptions"
/>
</LayoutMenu>
@@ -2,7 +2,7 @@
<aside>
<ViewerControlsButtonGroup
v-show="activePanel === 'none'"
class="absolute left-1/2 -translate-x-1/2 bottom-4 z-40"
class="absolute left-1/2 -translate-x-1/2 bottom-4 z-50"
>
<ViewerControlsButtonToggle
v-for="panel in panels"
@@ -16,7 +16,7 @@
<ViewerLayoutPanel
v-if="activePanel !== 'none'"
class="absolute left-1/2 -translate-x-1/2 bottom-4 z-30 flex p-1 items-center justify-between w-80"
class="absolute left-1/2 -translate-x-1/2 bottom-4 z-50 flex p-1 items-center justify-between w-80"
>
<span class="flex items-center">
<component :is="panels[activePanel].icon" class="h-4 w-4 ml-1 mr-1.5" />
@@ -1,12 +1,12 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<aside
class="absolute left-2 lg:left-0 top-[3.5rem] z-20 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible"
class="absolute left-2 lg:left-0 top-[3.5rem] z-40 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible lg:h-full"
:class="[
isEmbedEnabled
? ''
: 'lg:top-[3rem] lg:rounded-none lg:px-2 lg:max-h-[calc(100dvh-3rem)] lg:border-l-0 lg:border-t-0 lg:border-b-0',
hasActivePanel && 'h-full max-h-[calc(100dvh-7.875rem)] rounded-r-none'
hasActivePanel && 'h-full max-h-[calc(100dvh-8rem)] rounded-r-none'
]"
>
<div class="flex flex-col gap-2 py-1" :class="isEmbedEnabled ? '' : 'lg:py-2'">
@@ -99,15 +99,14 @@
<!-- Scrollable controls container -->
<div
v-show="activePanel !== 'none'"
ref="scrollableControlsContainer"
:class="[
'simple-scrollbar bg-foundation absolute z-10 left-[calc(2.5rem+1px)] top-[-1px] bottom-[-1px] overflow-y-auto overflow-x-visible border-outline-2 border border-l-0 rounded-lg rounded-tl-none rounded-bl-none',
'simple-scrollbar overflow-x-hidden bg-foundation absolute z-10 left-[calc(2.5rem+1px)] top-[-1px] bottom-[-1px] overflow-y-auto border-outline-2 border border-l-0 rounded-lg rounded-tl-none rounded-bl-none ',
hasActivePanel ? 'opacity-100' : 'opacity-0',
isEmbedEnabled ? '' : 'lg:left-[calc(3rem+1px)] lg:border-none lg:rounded-none'
]"
:style="`width: ${
isMobile ? 'calc(100vw - 3.75rem)' : `${width + 4}px`
}; overflow-x: visible !important;`"
:style="`width: ${isMobile ? 'calc(100vw - 3.6rem)' : `${width + 4}px`};`"
>
<ViewerModelsPanel
v-if="resourceItems.length !== 0 && activePanel === 'models'"
@@ -1,6 +1,10 @@
<template>
<aside class="absolute top-[3.75rem] z-20" :style="dynamicStyles">
<ViewerControlsButtonGroup direction="vertical">
<aside
ref="buttonContainer"
class="absolute top-[3.75rem] z-20"
:style="dynamicStyles"
>
<ViewerControlsButtonGroup ref="buttonContainer" direction="vertical">
<ViewerControlsButtonToggle
v-tippy="
getTooltipProps(getShortcutDisplayText(shortcuts.ZoomExtentsOrSelection), {
@@ -49,6 +53,7 @@ const { getTooltipProps } = useSmartTooltipDelay()
const activePanel = ref<ActivePanel>('none')
const menuContainer = ref<Nullable<HTMLElement>>(null)
const buttonContainer = ref<Nullable<HTMLElement>>(null)
const dynamicStyles = computed(() => {
if (props.sidebarOpen) {
@@ -75,7 +80,13 @@ registerShortcuts({
ZoomExtentsOrSelection: () => trackAndzoomExtentsOrSelection()
})
onClickOutside(menuContainer, () => {
activePanel.value = 'none'
})
onClickOutside(
menuContainer,
() => {
activePanel.value = 'none'
},
{
ignore: [buttonContainer]
}
)
</script>
@@ -63,7 +63,8 @@
<button
class="size-6 flex items-center justify-center rounded-md"
:class="[
showSettings && 'text-primary-focus bg-info-lighter',
showSettings &&
'text-primary-focus bg-info-lighter dark:text-foreground-on-primary',
!showSettings && 'text-foreground hover:bg-foundation-2'
]"
@click="showSettings = !showSettings"
@@ -79,7 +79,8 @@
<button
class="size-6 flex items-center justify-center rounded-md"
:class="[
showSettings && 'text-primary-focus bg-info-lighter',
showSettings &&
'text-primary-focus bg-info-lighter dark:text-foreground-on-primary',
!showSettings && 'text-foreground hover:bg-foundation-2'
]"
@click="showSettings = !showSettings"
+30 -13
View File
@@ -1,4 +1,6 @@
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
/**
* Smart tooltip delay composable
@@ -7,9 +9,13 @@ import type { MaybeNullOrUndefined } from '@speckle/shared'
* - First tooltip shows after a configurable delay (default 1 second)
* - Subsequent tooltips show instantly once user has shown intent
* - State resets after a period of inactivity (default 3 seconds)
* - Only shows tooltips on non-mobile devices
*/
export function useSmartTooltipDelay() {
const breakpoints = useBreakpoints(TailwindBreakpoints)
const isMobile = breakpoints.smaller('sm')
const initialDelay = 1000
const resetAfter = 3000
@@ -19,20 +25,31 @@ export function useSmartTooltipDelay() {
const getTooltipProps = (
content?: MaybeNullOrUndefined<string>,
additionalProps: Record<string, unknown> = {}
) => ({
content,
delay: hasShownAny.value ? 0 : initialDelay,
onShow: () => {
hasShownAny.value = true
if (resetTimer.value) {
clearTimeout(resetTimer.value)
) => {
// Don't show tooltips on mobile devices
if (isMobile.value) {
return {
content: null,
disabled: true,
...additionalProps
}
resetTimer.value = setTimeout(() => {
hasShownAny.value = false
}, resetAfter)
},
...additionalProps
})
}
return {
content,
delay: hasShownAny.value ? 0 : initialDelay,
onShow: () => {
hasShownAny.value = true
if (resetTimer.value) {
clearTimeout(resetTimer.value)
}
resetTimer.value = setTimeout(() => {
hasShownAny.value = false
}, resetAfter)
},
...additionalProps
}
}
const cleanup = () => {
if (resetTimer.value) {