From d93f28910ab1e2e6910b48090a0fde96a155a339 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 25 Oct 2026 17:24:00 +1100 Subject: [PATCH 1/2] Refreshing browse.html - do not redirect Prompt: when on browse.html the user gets redirected to localhost:3100/storare Co-authored-by: GPT-5.6 Luna --- README.md | 6 +++++ docs/navigation-design.md | 6 +++++ src/index.ts | 5 ++++ src/mainPage/profileHeading.ts | 10 +++++--- src/navigation/options.ts | 2 ++ src/outline/manager.js | 16 ++++++------ test/unit/mainPage/profileHeading.test.ts | 23 +++++++++++++++-- test/unit/outline/manager.test.ts | 30 +++++++++++++++++++++++ 8 files changed, 84 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 22795a69..862322cf 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ Volunteers are always welcome! ### Profile heading +Page shells can call `initializeProfileHeading(getPageUrl)` with a function +returning the currently browsed resource URL. Without an argument, the heading +uses `window.location.href`. The function is evaluated on each refresh, including +authentication changes, so shell-specific URL/query parsing stays in the shell. +Call `refreshProfileHeading(true)` after changing the shell's target. + The `solid-panes-profile-heading` component accepts presentation data through `profileData`. Its boolean `compact` property (also available as an attribute) halves the responsive avatar size and shows only the photo, name, and job title. diff --git a/docs/navigation-design.md b/docs/navigation-design.md index 4625e17f..de822b73 100644 --- a/docs/navigation-design.md +++ b/docs/navigation-design.md @@ -53,6 +53,12 @@ navigation controller, which always applies the same follow-up effects. mashlib. Those callers rely on it to push the history entry and the URL, so it is the one place that writes history, and it announces what it did. +Standalone shells such as `browse.html` can pass `preserveUrl: true` in the seventh argument when +they display a subject while keeping their own document URL (the subject is carried in the shell's +query parameters). The current history entry is still replaced with the displayed pane and +account-menu metadata, and navigation is announced from that updated state. This is intentionally +opt-in; ordinary `GotoSubject` callers continue to make the displayed subject the page URL. + Storage is an explicit browser view, not a resource's default pane. Its pane item renders the storage root with `solo` and `fullView` disabled, so the storage header, sidebar and listing remain visible even if the root has an index document. diff --git a/src/index.ts b/src/index.ts index 78f65756..dfbf79ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,6 +78,11 @@ export { byName // from paneRegistry } +export { + initializeProfileHeading, + refreshProfileHeading +} from './mainPage/profileHeading' + // export for simpler access by non-node scripts if (typeof window !== 'undefined') { ;(window).panes = { diff --git a/src/mainPage/profileHeading.ts b/src/mainPage/profileHeading.ts index 15720cc8..1008b4f6 100644 --- a/src/mainPage/profileHeading.ts +++ b/src/mainPage/profileHeading.ts @@ -23,6 +23,7 @@ let authReady: Promise | undefined let displayedPaneKey = '' let displayedSubjectUri = '' let loadingPaneKey = '' +let headingPageUrl = () => window.location.href const hideAnimations = new WeakMap() const showAnimations = new WeakMap() @@ -150,7 +151,8 @@ function ensureMountPoint (): HTMLElement | null { export async function refreshProfileHeading (force = false): Promise { await authReady - const pod = await getPodContext(window.location.href) + const pageUrl = headingPageUrl() + const pod = await getPodContext(pageUrl) const mount = ensureMountPoint() if (!mount) return @@ -165,7 +167,7 @@ export async function refreshProfileHeading (force = false): Promise { view, viaAccountMenu, viewer: authn.currentUser()?.value, - pageUrl: window.location.href, + pageUrl, owner: pod.owner, storageUris: pod.storagePanes.map(pane => pane.subject.value) }) @@ -243,7 +245,9 @@ export function placeProfileHeading (): void { ensureMountPoint() } -export function initializeProfileHeading (): Promise { +/** Shells browsing another resource can supply its current URL instead of the page URL. */ +export function initializeProfileHeading (getPageUrl = () => window.location.href): Promise { + headingPageUrl = getPageUrl ensureMountPoint() if (!profileSavedListenerRegistered) { diff --git a/src/navigation/options.ts b/src/navigation/options.ts index 00102f56..1edb35cc 100644 --- a/src/navigation/options.ts +++ b/src/navigation/options.ts @@ -17,6 +17,8 @@ export type FolderView = 'explorer' | 'storage' | 'container' export interface GotoSubjectOptions { source?: NavigationSource folderView?: FolderView + /** Keep the caller's document URL while displaying a different subject. */ + preserveUrl?: boolean } /** The folder view a `GotoSubject` caller asked for, if any. */ diff --git a/src/outline/manager.js b/src/outline/manager.js index 513de6d1..c56f3b87 100644 --- a/src/outline/manager.js +++ b/src/outline/manager.js @@ -1941,12 +1941,8 @@ export default function (context) { createSubjectBlock() } - if ( - solo && - dom && - dom.defaultView && - dom.defaultView.history - ) { + if (solo && dom && dom.defaultView && dom.defaultView.history) { + const preserveUrl = options && typeof options === 'object' && options.preserveUrl === true const currentState = dom.defaultView.history.state || {} const leavingPage = document.location.href !== subject.uri const stateObj = nextHistoryState(currentState, { @@ -1957,9 +1953,11 @@ export default function (context) { viaAccountMenu: !showNavbar }) try { - const currentUrl = new URL(document.location.href) - const targetUrl = new URL(subject.uri, document.location.href) - if (currentUrl.origin === targetUrl.origin) { + const currentUrl = new URL(dom.location.href) + const targetUrl = new URL(subject.uri, dom.location.href) + if (preserveUrl) { + dom.defaultView.history.replaceState(stateObj, '') + } else if (currentUrl.origin === targetUrl.origin) { if (leavingPage) { dom.defaultView.history.pushState(stateObj, subject.uri, subject.uri) } else if (JSON.stringify(currentState) !== JSON.stringify(stateObj)) { diff --git a/test/unit/mainPage/profileHeading.test.ts b/test/unit/mainPage/profileHeading.test.ts index 48f96837..86c6d016 100644 --- a/test/unit/mainPage/profileHeading.test.ts +++ b/test/unit/mainPage/profileHeading.test.ts @@ -4,16 +4,20 @@ import { lit, sym } from 'rdflib' import { ns } from 'solid-ui' import type { ProfileHeadingEditContext } from '../../../src/components/profile-heading/ProfileHeading' -const { getViewerModeMock, createHeadingEditDialogMock, pod } = vi.hoisted(() => ({ +const { getViewerModeMock, createHeadingEditDialogMock, getPodContextMock, pod } = vi.hoisted(() => ({ getViewerModeMock: vi.fn(), createHeadingEditDialogMock: vi.fn(), + getPodContextMock: vi.fn(), // What the page knows about the pod it is in pod: { current: { owner: undefined as string | undefined, storagePanes: [] as Array<{ subject: { value: string } }>, viewerStorages: [] as string[] } } })) vi.mock('../../../src/navigation/podContext', async (importOriginal) => ({ ...await importOriginal(), - getPodContext: async () => pod.current + getPodContext: async (url: string) => { + getPodContextMock(url) + return pod.current + } })) vi.mock('../../../src/components/profile-heading/editor/sections/heading/HeadingEditDialog', () => ({ @@ -220,6 +224,21 @@ describe('page-level profile heading', () => { checkUser.mockRestore() }) + it('uses the shell resource URL on each refresh and defaults back to the page URL', async () => { + let resourceUrl = 'https://heading-shell.example/profile/card#me' + await initializeProfileHeading(() => resourceUrl) + await refreshProfileHeading() + expect(getPodContextMock).toHaveBeenLastCalledWith(resourceUrl) + + resourceUrl = 'https://heading-shell.example/public/' + await refreshProfileHeading(true) + expect(getPodContextMock).toHaveBeenLastCalledWith(resourceUrl) + + await initializeProfileHeading() + await refreshProfileHeading() + expect(getPodContextMock).toHaveBeenLastCalledWith(window.location.href) + }) + it('hides extra information and editing in compact mode, then restores the full heading', async () => { const heading = document.createElement('solid-panes-profile-heading') as HTMLElement & { profileData: { diff --git a/test/unit/outline/manager.test.ts b/test/unit/outline/manager.test.ts index 07690f0d..2c08c86e 100644 --- a/test/unit/outline/manager.test.ts +++ b/test/unit/outline/manager.test.ts @@ -202,6 +202,36 @@ describe('manager', () => { expect(window.history.state).toMatchObject({ paneName: 'mock', paneUri: profile.uri, viaAccountMenu: true }) }) + it('updates navigation state while preserving the current URL', () => { + const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry } }) + const shellUrl = `${window.location.origin}/browse.html?uri=${encodeURIComponent(`${window.location.origin}/other/`)}` + const subject = sym(`${window.location.origin}/other/`) + const pane = { ...MockPane, subject } + window.history.replaceState({ existing: true }, '', shellUrl) + + const seen: NavigatedDetail[] = [] + const stop = onNavigated(detail => seen.push(detail)) + manager.GotoSubject(subject, false, pane, true, undefined, undefined, { + folderView: 'explorer', + preserveUrl: true + }) + stop() + + expect(window.location.href).toBe(shellUrl) + expect(window.history.state).toMatchObject({ + existing: true, + paneName: 'mock', + paneUri: subject.uri + }) + expect(seen.at(-1)).toMatchObject({ + url: shellUrl, + state: { + view: { kind: 'pane', pane: 'mock', subject: subject.uri }, + viaAccountMenu: false + } + }) + }) + it('displays a navbar pane\'s own subject, and the page subject for panes without one', () => { const manager = new OutlineManager({ dom: document, session: { paneRegistry: mockPaneRegistry } }) const page = sym(`${window.location.origin}/other/container/sub/`) From 23e38590d609bcb66cac8976e8d5760e6cbae95f Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 26 Oct 2026 08:53:24 +1100 Subject: [PATCH 2/2] update version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 754d12d0..7bca6460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-panes", - "version": "5.1.0-2", + "version": "5.1.0-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-panes", - "version": "5.1.0-2", + "version": "5.1.0-3", "license": "MIT", "dependencies": { "@lit/context": "^1.1.6", diff --git a/package.json b/package.json index 7a501c5a..52712e2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-panes", - "version": "5.1.0-2", + "version": "5.1.0-3", "description": "Solid-compatible Panes: applets and views for the mashlib and databrowser", "main": "dist/index.cjs.js", "types": "dist/index.d.ts",