From 8d95884c6557cd15b3aab7ff79e280a457d6e572 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Wed, 22 Jul 2026 06:22:13 -0700 Subject: [PATCH 01/13] Refactor immersive editor logic into a composable and move shared styles --- .../immersive-editor/EditorDrawer.vue | 199 ++---------------- frontend/src/composables/ImmersiveDrawer.ts | 111 ++++++++++ .../stylesheets/ff-immersive-editor.scss | 93 ++++++++ 3 files changed, 216 insertions(+), 187 deletions(-) create mode 100644 frontend/src/composables/ImmersiveDrawer.ts diff --git a/frontend/src/components/immersive-editor/EditorDrawer.vue b/frontend/src/components/immersive-editor/EditorDrawer.vue index c0e5b6c549..4557b44c24 100644 --- a/frontend/src/components/immersive-editor/EditorDrawer.vue +++ b/frontend/src/components/immersive-editor/EditorDrawer.vue @@ -116,20 +116,15 @@ import { HomeIcon, XMarkIcon } from '@heroicons/vue/20/solid' import { ArrowLeftIcon } from '@heroicons/vue/24/outline' import { storeToRefs } from 'pinia' -import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' -import { useRoute } from 'vue-router' +import { computed, nextTick, onMounted, watch } from 'vue' import getAppOrchestrator from '../../services/app.orchestrator' import ResizeBar from '../ResizeBar.vue' import EditorDrawerSettings from './EditorDrawerSettings.vue' +import { useImmersiveDrawer } from '@/composables/ImmersiveDrawer' import { useContextStore } from '@/stores/context.js' -import { useUxDrawersStore } from '@/stores/ux-drawers.js' - -const DRAWER_MIN_WIDTH = 310 -const DRAWER_MAX_VIEWPORT_MARGIN = 200 -const DRAWER_MAX_WIDTH_RATIO = 0.9 const props = defineProps({ navigation: { @@ -148,42 +143,19 @@ const props = defineProps({ const emit = defineEmits(['resizing']) -const drawersStore = useUxDrawersStore() const contextStore = useContextStore() -const { editorImmersiveDrawer } = storeToRefs(drawersStore) const { editorEntityType } = storeToRefs(contextStore) -const route = useRoute() - -const resizing = ref(false) -const startX = ref(0) -const startWidth = ref(0) -const windowWidth = ref(window.innerWidth) - -const isEditorResizing = computed(() => resizing.value) -const drawerStyle = computed(() => { - if (!editorImmersiveDrawer.value.state) return {} - const width = Math.min( - editorImmersiveDrawer.value.width, - windowWidth.value * DRAWER_MAX_WIDTH_RATIO, - windowWidth.value - DRAWER_MAX_VIEWPORT_MARGIN - ) - return { width: `${width}px`, order: editorImmersiveDrawer.value.side === 'right' ? 1 : -1 } -}) - -const hasStackedView = computed(() => editorImmersiveDrawer.value.viewStack.length > 0) -const currentStackView = computed(() => { - const stack = editorImmersiveDrawer.value.viewStack - return stack[stack.length - 1] || null -}) -const stackedActions = computed(() => { - const view = currentStackView.value - if (!view?.actions) return [] - return view.actions.filter(action => { - if (typeof action.hidden === 'function') return !action.hidden() - return !action.hidden - }) -}) +const { + drawersStore, + editorImmersiveDrawer, + isResizing: isEditorResizing, + drawerStyle, + hasStackedView, + currentStackView, + stackedActions, + startResize: startEditorResize +} = useImmersiveDrawer({ onResizingChange: resizing => emit('resizing', resizing) }) const homeRoute = computed(() => { if (!props.entity?.id) return null @@ -211,160 +183,13 @@ function notifyDrawerState () { }) } -function handleResize (e) { - if (!resizing.value) return - - const isLeftSide = editorImmersiveDrawer.value.side === 'left' - const delta = isLeftSide - ? e.clientX - startX.value - : startX.value - e.clientX - - const newWidth = Math.min( - Math.max(DRAWER_MIN_WIDTH, startWidth.value + delta), - window.innerWidth * DRAWER_MAX_WIDTH_RATIO, - window.innerWidth - DRAWER_MAX_VIEWPORT_MARGIN - ) - - drawersStore.setEditorImmersiveDrawerWidth(newWidth) -} - -function stopResize () { - resizing.value = false - emit('resizing', false) - document.removeEventListener('mousemove', handleResize) - document.removeEventListener('mouseup', stopResize) -} - -function startEditorResize (e) { - resizing.value = true - emit('resizing', true) - startX.value = e.clientX - startWidth.value = editorImmersiveDrawer.value.width - - document.addEventListener('mousemove', handleResize) - document.addEventListener('mouseup', stopResize) -} - -function onWindowResize () { - windowWidth.value = window.innerWidth -} - watch(() => editorImmersiveDrawer.value.state, () => { nextTick(notifyDrawerState) }) -watch(() => route.name, () => { - drawersStore.clearEditorImmersiveViewStack() -}) - onMounted(() => { - drawersStore.setEditorImmersiveActive(true) - window.addEventListener('resize', onWindowResize) nextTick(notifyDrawerState) }) -onUnmounted(() => { - drawersStore.setEditorImmersiveActive(false) - drawersStore.clearEditorImmersiveViewStack() - window.removeEventListener('resize', onWindowResize) - document.removeEventListener('mousemove', handleResize) - document.removeEventListener('mouseup', stopResize) -}) - defineExpose({ notifyDrawerState }) - - diff --git a/frontend/src/composables/ImmersiveDrawer.ts b/frontend/src/composables/ImmersiveDrawer.ts new file mode 100644 index 0000000000..37530e1cfa --- /dev/null +++ b/frontend/src/composables/ImmersiveDrawer.ts @@ -0,0 +1,111 @@ +import { storeToRefs } from 'pinia' +import { computed, onMounted, onUnmounted, ref, watch } from 'vue' +import { useRoute } from 'vue-router' + +import { useUxDrawersStore } from '@/stores/ux-drawers.js' + +const DRAWER_MIN_WIDTH = 310 +const DRAWER_MAX_VIEWPORT_MARGIN = 200 +const DRAWER_MAX_WIDTH_RATIO = 0.9 + +interface ImmersiveDrawerOptions { + onResizingChange?: (resizing: boolean) => void +} + +export function useImmersiveDrawer (options: ImmersiveDrawerOptions = {}) { + const { onResizingChange } = options + const drawersStore = useUxDrawersStore() + const { editorImmersiveDrawer } = storeToRefs(drawersStore) + const route = useRoute() + + const resizing = ref(false) + const startX = ref(0) + const startWidth = ref(0) + const windowWidth = ref(window.innerWidth) + + const isResizing = computed(() => resizing.value) + + const drawerStyle = computed(() => { + if (!editorImmersiveDrawer.value.state) return {} + const width = Math.min( + editorImmersiveDrawer.value.width, + windowWidth.value * DRAWER_MAX_WIDTH_RATIO, + windowWidth.value - DRAWER_MAX_VIEWPORT_MARGIN + ) + return { width: `${width}px`, order: editorImmersiveDrawer.value.side === 'right' ? 1 : -1 } + }) + + const hasStackedView = computed(() => editorImmersiveDrawer.value.viewStack.length > 0) + const currentStackView = computed(() => { + const stack = editorImmersiveDrawer.value.viewStack + return stack[stack.length - 1] || null + }) + const stackedActions = computed(() => { + const view = currentStackView.value + if (!view?.actions) return [] + return view.actions.filter((action: { hidden?: unknown }) => { + if (typeof action.hidden === 'function') return !action.hidden() + return !action.hidden + }) + }) + + function handleResize (e: MouseEvent) { + if (!resizing.value) return + const isLeftSide = editorImmersiveDrawer.value.side === 'left' + const delta = isLeftSide ? e.clientX - startX.value : startX.value - e.clientX + const newWidth = Math.min( + Math.max(DRAWER_MIN_WIDTH, startWidth.value + delta), + window.innerWidth * DRAWER_MAX_WIDTH_RATIO, + window.innerWidth - DRAWER_MAX_VIEWPORT_MARGIN + ) + drawersStore.setEditorImmersiveDrawerWidth(newWidth) + } + + function stopResize () { + resizing.value = false + onResizingChange?.(false) + document.removeEventListener('mousemove', handleResize) + document.removeEventListener('mouseup', stopResize) + } + + function startResize (e: MouseEvent) { + resizing.value = true + onResizingChange?.(true) + startX.value = e.clientX + startWidth.value = editorImmersiveDrawer.value.width + document.addEventListener('mousemove', handleResize) + document.addEventListener('mouseup', stopResize) + } + + function onWindowResize () { + windowWidth.value = window.innerWidth + } + + watch(() => route.name, () => { + drawersStore.clearEditorImmersiveViewStack() + }) + + onMounted(() => { + drawersStore.setEditorImmersiveActive(true) + window.addEventListener('resize', onWindowResize) + }) + + onUnmounted(() => { + drawersStore.setEditorImmersiveActive(false) + drawersStore.clearEditorImmersiveViewStack() + window.removeEventListener('resize', onWindowResize) + document.removeEventListener('mousemove', handleResize) + document.removeEventListener('mouseup', stopResize) + }) + + return { + drawersStore, + editorImmersiveDrawer, + isResizing, + drawerStyle, + hasStackedView, + currentStackView, + stackedActions, + startResize + } +} diff --git a/frontend/src/ui-components/stylesheets/ff-immersive-editor.scss b/frontend/src/ui-components/stylesheets/ff-immersive-editor.scss index 6337b0507e..02515b6d4f 100644 --- a/frontend/src/ui-components/stylesheets/ff-immersive-editor.scss +++ b/frontend/src/ui-components/stylesheets/ff-immersive-editor.scss @@ -145,3 +145,96 @@ } } } + +.ff--immersive-editor-wrapper .tabs-wrapper.drawer { + &.pinned { + position: relative; + left: auto; + top: auto; + height: 100%; + transform: none; + box-shadow: none; + + &.open { + border-right: 1px solid var(--ff-color-border); + + &.side-right { + border-right: none; + border-left: 1px solid var(--ff-color-border); + } + } + } + + &:not(.pinned).side-right { + left: auto; + right: 0; + transform: translateX(100%); + + &.open { + transform: translateX(0); + box-shadow: -5px 0 8px rgba(0, 0, 0, 0.10); + } + } + + .ff-layout--immersive--fullscreen &:not(.pinned) { + top: 0; + height: 100%; + } + + &.side-right .resize-bar { + left: 0; + right: auto; + border-right: none; + border-left: 1px solid var(--ff-color-border-strong); + + &::before { + left: -3px; + } + } + + .header, + .header--stacked { + min-height: 46px; + } + + .drawer-header-btn { + align-self: stretch; + background: none; + border: none; + padding: 0 15px; + color: var(--ff-color-text-subtle); + font: inherit; + display: flex; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + + .ff-btn--icon { + width: 18px; + height: 18px; + } + + &:hover { + background: var(--ff-color-bg-surface-raised); + color: var(--ff-color-text-muted); + } + + &:active { + background: var(--ff-color-bg-emphasis); + } + } + + .editor-drawer-stack-title { + flex: 1; + font-weight: 600; + font-size: 14px; + color: var(--ff-color-text); + padding: 0 10px; + display: flex; + align-items: center; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} From b5073c418647e6c65bb51de44b5c003696bb8699 Mon Sep 17 00:00:00 2001 From: Noley Holland Date: Tue, 21 Jul 2026 13:20:51 -0700 Subject: [PATCH 02/13] Implement dashboards drawer and side nav entry with shared helpers --- frontend/src/components/dashboard/index.vue | 72 ++++---- .../immersive-editor/DashboardDrawer.vue | 123 +++++++++++++ frontend/src/composables/TeamDashboards.ts | 87 +++++++++ frontend/src/pages/team/Dashboards/Viewer.vue | 168 ++++++++++++++++++ frontend/src/pages/team/Dashboards/index.vue | 78 ++++++++ frontend/src/pages/team/index.vue | 15 +- frontend/src/pages/team/routes.js | 19 ++ frontend/src/stores/product-expert.js | 12 ++ frontend/src/stores/ux-navigation.js | 13 +- 9 files changed, 547 insertions(+), 40 deletions(-) create mode 100644 frontend/src/components/immersive-editor/DashboardDrawer.vue create mode 100644 frontend/src/composables/TeamDashboards.ts create mode 100644 frontend/src/pages/team/Dashboards/Viewer.vue create mode 100644 frontend/src/pages/team/Dashboards/index.vue diff --git a/frontend/src/components/dashboard/index.vue b/frontend/src/components/dashboard/index.vue index 167346a7df..7bedff74d4 100644 --- a/frontend/src/components/dashboard/index.vue +++ b/frontend/src/components/dashboard/index.vue @@ -9,52 +9,48 @@

The instance must be running to view the dashboard.

-