Merge branch 'main' into andrew/web-4174-resetting-section-box-shouldnt-disable-tool

This commit is contained in:
andrewwallacespeckle
2025-09-23 11:52:45 +02:00
15 changed files with 114 additions and 52 deletions
@@ -2,10 +2,13 @@
<div
class="bg-foundation border border-outline-3 rounded-xl shadow-md h-10 flex items-center"
>
<div class="flex items-center justify-between md:space-x-1 p-1">
<FormButton class="hidden md:flex">Share</FormButton>
<div class="flex items-center justify-between space-x-1 p-1">
<FormButton>Share</FormButton>
<PresentationFloatingPanelButton class="hidden md:flex" @click="toggleFullscreen">
<PresentationFloatingPanelButton
class="hidden md:flex touch:hidden"
@click="toggleFullscreen"
>
<LucideFullscreen class="size-4" />
</PresentationFloatingPanelButton>
@@ -1,8 +1,8 @@
<template>
<aside
class="bg-foundation h-48 md:h-dvh w-full md:w-64 xl:w-80 border-t md:border-t-0 md:border-l border-outline-3 py-5 px-4"
class="bg-foundation h-48 lg:h-dvh w-full lg:w-64 xl:w-80 border-t lg:border-t-0 lg:border-l border-outline-3 py-5 px-4"
>
<div class="hidden md:flex items-center justify-end space-x-0.5">
<div class="hidden lg:flex items-center justify-end space-x-0.5">
<div
v-tippy="
canUpdateSlide ? undefined : 'You do not have permission to edit this slide'
@@ -29,14 +29,21 @@
<h1 v-if="currentSlide?.name" class="text-xl font-medium text-foreground px-2">
{{ currentSlide?.name }}
</h1>
<FormButton
v-if="canUpdate"
:icon-left="LucidePencilLine"
color="subtle"
hide-text
class="md:hidden"
@click="isSlideEditDialogOpen = true"
/>
<div class="lg:hidden flex items-center gap-x-1">
<FormButton
v-if="canUpdate"
:icon-left="LucidePencilLine"
color="subtle"
hide-text
@click="isSlideEditDialogOpen = true"
/>
<FormButton
:icon-left="LucideX"
color="subtle"
hide-text
@click="$emit('close')"
/>
</div>
</div>
<p
@@ -1,5 +1,5 @@
<template>
<div class="w-full md:w-auto">
<div class="w-full sm:w-auto">
<div class="fixed inset-0 z-10 md:hidden">
<div class="absolute inset-0 bg-black/50" />
</div>
@@ -20,12 +20,6 @@
</p>
</div>
</NuxtLink>
<UserAvatar
v-if="isLoggedIn"
size="sm"
class="ml-auto flex-shrink-0"
:user="activeUser"
/>
</section>
<section
class="flex-1 flex justify-center simple-scrollbar overflow-y-auto mt-3 pb-3 px-3"
@@ -59,7 +53,7 @@ graphql(`
}
`)
const { isLoggedIn, activeUser } = useActiveUser()
const { isLoggedIn } = useActiveUser()
const {
response: { workspace }
} = useInjectedPresentationState()
@@ -12,9 +12,9 @@
<PresentationActions
v-if="!hideUi"
v-model:is-sidebar-open="isInfoSidebarOpen"
class="absolute bottom-4 md:top-4 right-4 z-20"
class="absolute bottom-4 lg:top-4 right-4 z-20"
:class="{
'bottom-52 lg:bottom-auto md:right-[17rem] xl:right-[21rem]':
'bottom-52 lg:bottom-auto lg:right-[17rem] xl:right-[21rem]':
isInfoSidebarOpen
}"
@toggle-sidebar="isInfoSidebarOpen = !isInfoSidebarOpen"
@@ -23,12 +23,17 @@
<PresentationSlideIndicator
v-if="!isViewerLoading"
:show-slide-list="!isLeftSidebarOpen"
class="absolute top-1/2 translate-y-[calc(-50%+25px)] z-20"
:class="[isLeftSidebarOpen ? 'lg:left-[14.75rem] hidden md:block' : 'left-0']"
class="absolute top-1/2 z-20"
:class="[
isInfoSidebarOpen
? 'translate-y-[calc(-50%+25px-6rem)] lg:translate-y-[-50%]'
: 'translate-y-[-50%]',
isLeftSidebarOpen ? 'lg:left-[14.75rem] hidden md:block' : 'left-0'
]"
/>
<PresentationSpeckleLogo
class="absolute right-4 z-30 top-4 md:top-auto md:bottom-4"
class="absolute right-4 z-30 top-4 lg:top-auto lg:bottom-4"
:class="[isInfoSidebarOpen ? '' : '']"
/>
@@ -37,7 +42,7 @@
class="absolute left-0 top-0 md:relative flex-shrink-0 z-30"
/>
<div class="flex-1 z-0">
<div class="flex-1 z-0 flex flex-col lg:flex-row">
<Component
:is="presentation ? ViewerWrapper : 'div'"
:group="presentation"
@@ -45,20 +50,20 @@
@loading-change="onLoadingChange"
@progress-change="onProgressChange"
/>
</div>
<PresentationInfoSidebar
v-if="isInfoSidebarOpen"
class="flex-shrink-0 z-20"
@close="isInfoSidebarOpen = false"
/>
<PresentationInfoSidebar
v-if="isInfoSidebarOpen"
class="flex-shrink-0 z-20"
@close="isInfoSidebarOpen = false"
/>
</div>
<PresentationControls
:hide-ui="hideUi"
class="absolute left-4 md:left-1/2 md:-translate-x-1/2"
class="absolute left-4 lg:left-1/2 lg:-translate-x-1/2"
:class="[
isInfoSidebarOpen ? 'bottom-52 md:bottom-4' : 'bottom-4',
isLeftSidebarOpen ? 'hidden md:flex' : ''
isInfoSidebarOpen ? 'bottom-52 lg:bottom-4' : 'bottom-4',
isLeftSidebarOpen ? 'hidden md:flex md:left-64' : ''
]"
/>
</div>
@@ -6,7 +6,7 @@
<img
:src="slide?.screenshot"
:alt="slide?.name"
class="w-full object-cover rounded-lg border border-outline-3"
class="w-full object-cover rounded-lg border border-outline-3 h-64"
/>
<FormTextInput
v-model="name"
@@ -11,7 +11,7 @@
<div
v-if="showSlideList"
class="absolute top-[calc(50%+25px)] -translate-y-1/2 max-h-[75vh] overflow-y-auto w-56 simple-scrollbar bg-foundation border border-outline-3 rounded-xl p-3 shadow-md transition-all duration-300 ease-out opacity-0 invisible group-hover:opacity-100 group-hover:visible -translate-x-5 group-hover:translate-x-0"
class="hidden lg:block absolute top-[calc(50%+25px)] -translate-y-1/2 max-h-[75vh] overflow-y-auto w-56 simple-scrollbar bg-foundation border border-outline-3 rounded-xl p-3 shadow-md transition-all duration-300 ease-out opacity-0 invisible group-hover:opacity-100 group-hover:visible -translate-x-5 group-hover:translate-x-0"
>
<PresentationSlideList class="w-full" hide-title />
</div>
@@ -1,11 +1,15 @@
<template>
<li :class="{ 'pb-1 last:pb-0': hideTitle }">
<li class="w-full" :class="{ 'pb-1 last:pb-0': hideTitle }">
<button
class="bg-foundation-page rounded-xl overflow-hidden border border-outline-3 transition-all duration-200 hover:!border-outline-4"
class="bg-foundation-page rounded-xl overflow-hidden border border-outline-3 transition-all duration-200 hover:!border-outline-4 w-full"
:class="[isCurrentSlide ? '!border-outline-5' : '']"
@click="onSelectSlide"
>
<img :src="slide.screenshot" :alt="slide.name" class="w-full h-28 object-cover" />
<img
:src="slide.screenshot"
:alt="slide.name"
class="w-full h-28 object-contain"
/>
</button>
<p v-if="!hideTitle" class="text-body-3xs leading-none text-foreground my-2">
{{ slideIndex }}. {{ slide.name }}
@@ -2,20 +2,27 @@
<template>
<div
ref="resizableElement"
class="relative sm:absolute z-10 right-0 overflow-hidden w-screen bottom-0 sm:bottom-auto sm:top-[3.5rem] lg:top-[3rem] sm:right-2 lg:right-0 h-[40dvh] sm:h-[calc(100dvh-8rem)] lg:h-[calc(100dvh-3rem)] sm:max-w-[264px]"
class="relative sm:absolute z-10 right-0 overflow-hidden w-screen bottom-0 sm:bottom-auto sm:right-2 h-[40dvh] sm:max-w-[276px]"
:style="isLgOrLarger ? { maxWidth: width + 'px' } : {}"
:class="[open ? '' : 'pointer-events-none']"
:class="[
open ? '' : 'pointer-events-none',
isEmbedEnabled
? 'sm:top-2 sm:h-[calc(100dvh-8rem)]'
: 'sm:top-[3.5rem] lg:h-[calc(100dvh-3rem)] lg:top-[3rem] lg:right-0 sm:h-[calc(100dvh-8rem)]'
]"
>
<div class="flex h-full" :class="open ? '' : 'sm:translate-x-[100%]'">
<!-- Resize Handle -->
<div
v-if="!isEmbedEnabled"
ref="resizeHandle"
class="absolute h-full max-h-[calc(100dvh-3rem)] w-4 transition border-l sm:rounded-lg lg:rounded-none hover:border-l-[2px] border-outline-2 hover:border-primary hidden lg:flex items-center cursor-ew-resize z-30"
@mousedown="startResizing"
/>
<div
class="flex flex-col w-full h-full relative z-20 overflow-hidden sm:rounded-lg lg:rounded-none border-l border-t sm:border lg:border-0 lg:border-l border-outline-2 bg-foundation"
class="flex flex-col w-full h-full relative z-20 overflow-hidden sm:rounded-lg border-l border-t sm:border border-outline-2 bg-foundation"
:class="!isEmbedEnabled ? 'lg:rounded-none lg:border-0 lg:border-l' : ''"
>
<div
class="h-10 pl-4 pr-2 flex items-center justify-between border-b border-outline-2"
@@ -39,6 +46,7 @@
import { ref, onMounted } from 'vue'
import { useEventListener, useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
defineProps<{
open: boolean
@@ -52,12 +60,13 @@ const emit = defineEmits<{
const resizableElement = ref(null)
const resizeHandle = ref(null)
const isResizing = ref(false)
const width = ref(280)
const width = ref(276)
let startWidth = 0
let startX = 0
const breakpoints = useBreakpoints(TailwindBreakpoints)
const isLgOrLarger = breakpoints.greaterOrEqual('lg')
const { isEnabled: isEmbedEnabled } = useEmbed()
const startResizing = (event: MouseEvent) => {
event.preventDefault()
@@ -1,11 +1,11 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<aside
class="absolute left-2 lg:left-0 z-50 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible lg:h-full focus-visible:outline-none"
class="absolute left-2 z-50 flex rounded-lg border border-outline-2 bg-foundation px-1 overflow-visible focus-visible:outline-none"
:class="[
isEmbedEnabled
? 'top-[0.5rem]'
: 'top-[3.5rem] 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',
: 'top-[3.5rem] 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 lg:h-full lg:left-0',
hasActivePanel && 'h-full max-h-[calc(100dvh-8rem)] rounded-r-none'
]"
>
@@ -121,7 +121,7 @@
<!-- Resize handle -->
<div
v-if="activePanel !== 'none'"
v-if="activePanel !== 'none' && !isEmbedEnabled"
ref="resizeHandle"
class="absolute h-full max-h-[calc(100dvh-3rem)] w-4 transition border-l hover:border-l-[2px] border-outline-2 hover:border-primary hidden lg:flex items-center cursor-ew-resize z-30"
:style="`left:${width + 52}px;`"
@@ -289,7 +289,7 @@ const throttledHandleMouseMove = useThrottleFn((event: MouseEvent) => {
)
panelExtensionWidth.value = newWidth
}
}, 150)
}, 50)
if (import.meta.client) {
useResizeObserver(scrollableControlsContainer, (entries) => {
@@ -17,7 +17,7 @@
/>
<template #title-actions>
<div
class="flex gap-0.5 items-center opacity-0 group-hover/disclosure:opacity-100"
class="flex gap-0.5 items-center lg:opacity-0 lg:group-hover/disclosure:opacity-100"
@click.stop
>
<LayoutMenu
@@ -36,6 +36,10 @@ export class HighlightExtension extends SelectionExtension {
}
this.options = highlightMaterialData
}
/** Disable default click events - highlighting is controlled through state only */
protected override onObjectClicked() {}
protected override onObjectDoubleClick() {}
}
/**
@@ -1,4 +1,5 @@
type UserMeta {
flag(key: String!): Boolean!
newWorkspaceExplainerDismissed: Boolean!
speckleConBannerDismissed: Boolean!
intelligenceCommunityStandUpBannerDismissed: Boolean!
@@ -6,6 +7,7 @@ type UserMeta {
}
type UserMetaMutations {
setFlag(key: String!, value: Boolean!): Boolean!
setNewWorkspaceExplainerDismissed(value: Boolean!): Boolean!
setSpeckleConBannerDismissed(value: Boolean!): Boolean!
setIntelligenceCommunityStandUpBannerDismissed(value: Boolean!): Boolean!
+9
View File
@@ -298,10 +298,19 @@ export const StreamFavorites = buildTableHelper('stream_favorites', [
'cursor'
])
export const UsersMetaFlags = ['presentationsFeatureNudgeDismissed'] as const
type UsersMetaFlag = (typeof UsersMetaFlags)[number]
export const isUsersMetaFlag = (key: string): key is UsersMetaFlag => {
return UsersMetaFlags.includes(key as UsersMetaFlag)
}
export const UsersMeta = buildMetaTableHelper(
'users_meta',
['userId', 'key', 'value', 'createdAt', 'updatedAt'],
[
...UsersMetaFlags,
'isOnboardingFinished',
'onboardingStreamId',
'activeWorkspace',
@@ -4955,14 +4955,21 @@ export type UserGendoAiCredits = {
export type UserMeta = {
__typename?: 'UserMeta';
flag: Scalars['Boolean']['output'];
intelligenceCommunityStandUpBannerDismissed: Scalars['Boolean']['output'];
legacyProjectsExplainerCollapsed: Scalars['Boolean']['output'];
newWorkspaceExplainerDismissed: Scalars['Boolean']['output'];
speckleConBannerDismissed: Scalars['Boolean']['output'];
};
export type UserMetaFlagArgs = {
key: Scalars['String']['input'];
};
export type UserMetaMutations = {
__typename?: 'UserMetaMutations';
setFlag: Scalars['Boolean']['output'];
setIntelligenceCommunityStandUpBannerDismissed: Scalars['Boolean']['output'];
setLegacyProjectsExplainerCollapsed: Scalars['Boolean']['output'];
setNewWorkspaceExplainerDismissed: Scalars['Boolean']['output'];
@@ -4970,6 +4977,12 @@ export type UserMetaMutations = {
};
export type UserMetaMutationsSetFlagArgs = {
key: Scalars['String']['input'];
value: Scalars['Boolean']['input'];
};
export type UserMetaMutationsSetIntelligenceCommunityStandUpBannerDismissedArgs = {
value: Scalars['Boolean']['input'];
};
@@ -8557,6 +8570,7 @@ export type UserGendoAiCreditsResolvers<ContextType = GraphQLContext, ParentType
};
export type UserMetaResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserMeta'] = ResolversParentTypes['UserMeta']> = {
flag?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaFlagArgs, 'key'>>;
intelligenceCommunityStandUpBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
legacyProjectsExplainerCollapsed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
newWorkspaceExplainerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
@@ -8565,6 +8579,7 @@ export type UserMetaResolvers<ContextType = GraphQLContext, ParentType extends R
};
export type UserMetaMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserMetaMutations'] = ResolversParentTypes['UserMetaMutations']> = {
setFlag?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetFlagArgs, 'key' | 'value'>>;
setIntelligenceCommunityStandUpBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetIntelligenceCommunityStandUpBannerDismissedArgs, 'value'>>;
setLegacyProjectsExplainerCollapsed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetLegacyProjectsExplainerCollapsedArgs, 'value'>>;
setNewWorkspaceExplainerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetNewWorkspaceExplainerDismissedArgs, 'value'>>;
@@ -17,7 +17,7 @@ import {
lookupUsersFactory,
bulkLookupUsersFactory
} from '@/modules/core/repositories/users'
import { Users, UsersMeta } from '@/modules/core/dbSchema'
import { isUsersMetaFlag, Users, UsersMeta } from '@/modules/core/dbSchema'
import { throwForNotHavingServerRole } from '@/modules/shared/authz'
import {
deleteAllUserInvitesFactory,
@@ -25,7 +25,7 @@ import {
findServerInvitesFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import db from '@/db/knex'
import { BadRequestError } from '@/modules/shared/errors'
import { BadRequestError, InvalidArgumentError } from '@/modules/shared/errors'
import {
updateUserAndNotifyFactory,
deleteUserFactory,
@@ -502,6 +502,16 @@ export default {
meta: () => ({})
},
UserMetaMutations: {
setFlag: async (_parent, { key, value }, ctx) => {
if (!isUsersMetaFlag(key)) {
throw new InvalidArgumentError(`User flag ${key} is not known.`)
}
const meta = metaHelpers(Users, db)
const res = await meta.set(ctx.userId!, key, value)
return !!res.value
},
setLegacyProjectsExplainerCollapsed: async (_parent, args, ctx) => {
const meta = metaHelpers(Users, db)
const res = await meta.set(