Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/devframe/src/utils/remote-assets.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AddressInfo } from 'node:net'
import type { MockInstance } from 'vitest'
import type { RemoteAssets, RemoteAssetsErrorMessage, RemoteAssetsStore } from '../types/remote-assets'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
import { createServer } from 'node:http'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
Expand All @@ -12,8 +12,11 @@ import { DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE } from '../constants'
import { resolveStaticAssetsSource } from './remote-assets'
import { serveStaticHandler } from './serve-static'

// `realpathSync` because module resolution reports realpaths, while the system
// temp dir is a symlink on macOS (`/var` → `/private/var`) — a path built from
// the raw `mkdtempSync` result would never match a resolved one.
function makeTmp(): string {
return mkdtempSync(join(tmpdir(), 'devframe-remote-assets-'))
return realpathSync(mkdtempSync(join(tmpdir(), 'devframe-remote-assets-')))
}

/** A fake CDN over a flat `filePath -> contents` map, answering the jsDelivr listing API + per-file URLs. */
Expand Down
8 changes: 4 additions & 4 deletions packages/hub-ui/src/client/components/dock/Dock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const isRpcTrusted = useIsRpcTrusted(context, (isTrusted) => {
else if (!isTrusted) {
// On revocation: close current tab and panel
context.docks.switchEntry(null)
context.panel.store.open = false
context.panel.session.open = false
}
})

Expand Down Expand Up @@ -183,7 +183,7 @@ const isMinimized = computed(() => {
// @ts-expect-error compatibility
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
return !context.panel.isDragging
&& !context.panel.store.open
&& !context.panel.session.open
&& !isHovering.value
&& !isTouchDevice
&& context.panel.store.inactiveTimeout
Expand Down Expand Up @@ -218,8 +218,8 @@ whenever(isMinimized, () => {
})

onMounted(() => {
if (context.panel.store.open && !isRpcTrusted.value)
context.panel.store.open = false
if (context.panel.session.open && !isRpcTrusted.value)
context.panel.session.open = false
if (isRpcTrusted.value)
bringUp()
recalculateCounter.value++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ function edgeStory(position: 'top' | 'right' | 'bottom' | 'left', open = true) {
{
entries: categorizedEntries,
selectedId: open ? 'overview' : null,
panel: { mode: 'edge', position, open, height: 40, width: 30 },
panel: { mode: 'edge', position, height: 40, width: 30 },
session: { open },
},
ctx => [
h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }),
Expand Down Expand Up @@ -75,7 +76,8 @@ export const CollapsedIdle: Story = {
{
entries: categorizedEntries,
selectedId: null,
panel: { mode: 'edge', position: 'bottom', open: false, inactiveTimeout: 0 },
panel: { mode: 'edge', position: 'bottom', inactiveTimeout: 0 },
session: { open: false },
settings: { autoCollapseEdgeToolbar: true },
},
ctx => [
Expand All @@ -93,7 +95,7 @@ export const WithGroup: Story = {
{
entries: groupedEntries,
selectedId: 'nuxt:overview',
panel: { mode: 'edge', position: 'bottom', open: true, height: 45 },
panel: { mode: 'edge', position: 'bottom', height: 45 },
},
ctx => [
h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }),
Expand Down
2 changes: 1 addition & 1 deletion packages/hub-ui/src/client/components/dock/DockEdge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const selectedEntry = computed(() => context.docks.selected)
const activeGroup = computed(() => getEntryGroup(context.docks.entries, selectedEntry.value))
const hasPanelContent = computed(() => {
const entry = selectedEntry.value
return context.panel.store.open
return context.panel.session.open
&& !!entry
&& entry.type !== 'action'
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const FloatReducedOverlap: Story = {
export const Edge: Story = {
render: () => ({
setup: () => mountWithContext(
{ entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', open: true, height: 45 } },
{ entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', height: 45 } },
ctx => h(DockEmbedded, { context: ctx }),
),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ useEventListener(window, 'mousedown', (e: MouseEvent) => {
return
if (isDockPopupOpen.value)
return
if (!props.context.panel.store.open || props.context.panel.isDragging || props.context.panel.isResizing)
if (!props.context.panel.session.open || props.context.panel.isDragging || props.context.panel.isResizing)
return

const matched = e.composedPath().find((_el) => {
Expand Down
20 changes: 19 additions & 1 deletion packages/hub-ui/src/client/components/views/ViewIframe.vue
Original file line number Diff line number Diff line change
Expand Up @@ -224,19 +224,37 @@ let onIframeLoad: (() => void) | undefined

onMounted(() => {
const existed = props.panes.has(paneKey.value)
// Restore the address-bar route persisted before the last reload: the dock
// that was selected then boots deep-linked to where the developer left it,
// instead of the entry's default url. `consumeBootRoute` hands the saved URL
// back only for that dock, and only once, so a later switch can't reuse it.
const bootUrl = props.context.panel.consumeBootRoute?.(props.entry.id) ?? props.entry.url
if (!existed && bootUrl !== currentUrl.value) {
currentUrl.value = bootUrl
editingUrl.value = bootUrl
}
// `src` is only assigned when the pane is first created, so re-mounting an
// existing iframe (tab switch) preserves its navigation/scroll/JS state. For
// a shared frame this is also the boot deep-link: the first member (or the
// anchor) to become visible seeds the src, and every later switch soft-navs.
const pane = props.panes.ensure(paneKey.value, {
src: props.entry.url,
src: bootUrl,
style: { boxShadow: 'none', outline: 'none' },
})
const iframe = pane.iframe

if (existed)
updateCurrentUrl()

// Persist this dock's live route while it is the selected one, so the next
// reload can restore it. Only the selected dock writes, so switching docks
// never overwrites another's saved route.
const panelSession = props.context.panel.session
watchEffect(() => {
if (props.context.docks.selectedId === props.entry.id)
panelSession.selectedDockRoute = currentUrl.value
})

// Listen for iframe load events
onIframeLoad = () => {
isIframeLoading.value = false
Expand Down
18 changes: 14 additions & 4 deletions packages/hub-ui/src/client/embedded/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { DockPanelStorage } from '@devframes/hub/client'
import type { DockPanelStorage, DockSessionStorage } from '@devframes/hub/client'
import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client'
import { useLocalStorage } from '@vueuse/core'
import { useLocalStorage, useSessionStorage } from '@vueuse/core'
import { applyPrimaryColor, setBranding } from '../state/branding'
import { DEFAULT_DOCK_PANEL_STORE } from '../state/docks'
import { DEFAULT_DOCK_PANEL_STORE, DEFAULT_DOCK_SESSION_STORE } from '../state/docks'
import { setupEmbeddedVisibility } from './visibility'

/**
Expand Down Expand Up @@ -59,13 +59,23 @@ async function mountDock(): Promise<void> {
{ mergeDefaults: true },
)

// Per-tab session UI state (open dock + its route). `sessionStorage`, not
// `localStorage`: selection is per-tab navigation state, so two tabs against
// the same server keep their own rather than fighting over a shared one. It
// survives a reload and is restored after the auth handshake.
const session = useSessionStorage<DockSessionStorage>(
'devframes-dock-session',
DEFAULT_DOCK_SESSION_STORE(),
{ mergeDefaults: true },
)

// Resolve branding before the dock exists so the primary color and logo are
// in place on the first paint. Read from `ConnectionMeta.configs.ui.branding`,
// carried by the connection we just established above.
const branding = setBranding(rpc.connectionMeta.configs?.ui?.branding || {})

const { createDocksContext } = await import('../state/context')
const context = await createDocksContext('embedded', rpc, state)
const context = await createDocksContext('embedded', rpc, state, session)
setDevframeClientContext(context)

const { DockEmbedded } = await import('../components/DockEmbedded')
Expand Down
13 changes: 12 additions & 1 deletion packages/hub-ui/src/client/standalone/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { DockSessionStorage } from '@devframes/hub/client'
import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client'
import { useSessionStorage } from '@vueuse/core'
import { watchEffect } from 'vue'
import { applyDocumentHead, applyPrimaryColor, setBranding } from '../state/branding'
import { isDark } from '../state/color-mode'
import { DEFAULT_DOCK_SESSION_STORE } from '../state/docks'

// The standalone viewer — a vanilla shell served at the hub base itself
// (`DevframeHubUi.viewer`): resolve the shared connection, build the docks
Expand Down Expand Up @@ -37,8 +40,16 @@ async function main(): Promise<void> {
const branding = setBranding(rpc.connectionMeta.configs?.ui?.branding || {})
applyDocumentHead(document, branding)

// Per-tab session UI state (which dock is open + its route). `sessionStorage`
// so a reload restores the selection after the auth handshake, per-tab.
const session = useSessionStorage<DockSessionStorage>(
'devframes-dock-session',
DEFAULT_DOCK_SESSION_STORE(),
{ mergeDefaults: true },
)

const { createDocksContext } = await import('../state/context')
const context = await createDocksContext('standalone', rpc)
const context = await createDocksContext('standalone', rpc, undefined, session)
setDevframeClientContext(context)

const { DockStandalone } = await import('../components/DockStandalone')
Expand Down
Loading
Loading