@@ -280,6 +280,65 @@ function WideSplitView({
280280 // (Settings: drill into a sub-form). Browser Back/Forward leave intent null
281281 // and re-derive from the pane crumb trail or a replace.
282282 const applyIntentRef = React . useRef < 'replace' | 'push' | null > ( null ) ;
283+ // Selection href the pane is already showing, so the URL-sync effect below can
284+ // tell "the URL changed" from "we changed the URL to match the pane".
285+ const appliedHrefRef = React . useRef < string | undefined > ( undefined ) ;
286+
287+ // `replace()` inside the pane (create screens swapping themselves for the
288+ // record they just created) goes through `navigateLink`, not `navigate`. In
289+ // selectionParam mode the pane is a mirror of the main URL, so applying such a
290+ // link to the pane alone leaves the address bar on the screen the user just
291+ // left: reloading a freshly created entity lands back on its empty "new" form.
292+ //
293+ // Apply the link to the pane exactly as before, then mirror the scene it
294+ // resolves to into the main URL. `appliedHrefRef` is primed so the sync effect
295+ // recognises the pane as already up to date and does not re-derive it.
296+ const navigateLinkMirroringUrl = React . useCallback (
297+ ( url : string , historyAction ?: 'add' | 'replace' | 'none' ) => {
298+ if ( ! selectionParam ) {
299+ detailNavigator . navigateLink ( url , historyAction ) ;
300+ return ;
301+ }
302+ let href : string | undefined ;
303+ try {
304+ const parsed = detailNavigator . parseLink ( url ) ;
305+ href =
306+ parsed ?. state ?. key && parsed . state . key !== rootKey
307+ ? encodeSelectionHref ( parsed . state . key , parsed . data )
308+ : undefined ;
309+ } catch {
310+ // Unresolvable link: let the pane deal with it and leave the URL alone.
311+ detailNavigator . navigateLink ( url , historyAction ) ;
312+ return ;
313+ }
314+ detailNavigator . navigateLink ( url , historyAction ) ;
315+ appliedHrefRef . current = href ;
316+ // A replaced screen must not stay reachable through in-app back either, so
317+ // drop its selection-history entry before the effect records the new one —
318+ // that keeps this stack in step with the browser history it mirrors.
319+ if ( historyAction === 'replace' ) {
320+ selectionStackRef . current . pop ( ) ;
321+ }
322+ const currentData = mainNavigator . stateContext . data ?? { } ;
323+ const next : Record < string , any > = { ...currentData } ;
324+ if ( href ) {
325+ next [ selectionParam ] = href ;
326+ } else {
327+ delete next [ selectionParam ] ;
328+ }
329+ mainNavigator . refresh (
330+ next ,
331+ historyAction === 'replace' ? 'replace' : 'add'
332+ ) ;
333+ } ,
334+ [
335+ detailNavigator ,
336+ encodeSelectionHref ,
337+ mainNavigator ,
338+ rootKey ,
339+ selectionParam ,
340+ ]
341+ ) ;
283342
284343 // Pushes from the master column select a new detail: they reset the pane to
285344 // [root, detail] instead of stacking on whatever was selected before.
@@ -290,13 +349,20 @@ function WideSplitView({
290349 // below. That keeps ONE history timeline (URL + browser/native Back) and
291350 // avoids the pane driving itself out of sync with the URL.
292351 const masterNavigator = React . useMemo ( ( ) => {
293- const selectViaUrl = ( key : string , params ?: any ) => {
352+ const selectViaUrl = (
353+ key : string ,
354+ params ?: any ,
355+ historyAction ?: 'add' | 'replace' | 'none'
356+ ) => {
294357 applyIntentRef . current = 'replace' ;
295358 const href = encodeSelectionHref ( key , params ) ;
296359 const currentData = mainNavigator . stateContext . data ?? { } ;
360+ if ( historyAction === 'replace' ) {
361+ selectionStackRef . current . pop ( ) ;
362+ }
297363 mainNavigator . refresh (
298364 { ...currentData , [ selectionParam as string ] : href } ,
299- 'add'
365+ historyAction === 'replace' ? 'replace' : 'add'
300366 ) ;
301367 } ;
302368 const selectDetail = ( key : string , params ?: any ) => {
@@ -311,6 +377,9 @@ function WideSplitView({
311377 if ( prop === 'navigate' ) {
312378 return selectionParam ? selectViaUrl : selectDetail ;
313379 }
380+ if ( selectionParam && prop === 'navigateLink' ) {
381+ return navigateLinkMirroringUrl ;
382+ }
314383 // In selectionParam mode the selection (and any deeper drill pushed
315384 // inside the detail pane) lives on the MAIN navigator's history. In-app
316385 // back (BackLink/pop) must therefore step through that history — the
@@ -332,6 +401,7 @@ function WideSplitView({
332401 selectionParam ,
333402 mainNavigator ,
334403 encodeSelectionHref ,
404+ navigateLinkMirroringUrl ,
335405 paneGoBack ,
336406 paneCanGoBack ,
337407 ] ) ;
@@ -340,13 +410,20 @@ function WideSplitView({
340410 // record a main-URL history entry when selectionParam is set). Distinct from
341411 // masterNavigator, which always replaces.
342412 const detailStackNavigator = React . useMemo ( ( ) => {
343- const stackViaUrl = ( key : string , params ?: any ) => {
344- applyIntentRef . current = 'push' ;
413+ const stackViaUrl = (
414+ key : string ,
415+ params ?: any ,
416+ historyAction ?: 'add' | 'replace' | 'none'
417+ ) => {
418+ applyIntentRef . current = historyAction === 'replace' ? 'replace' : 'push' ;
345419 const href = encodeSelectionHref ( key , params ) ;
346420 const currentData = mainNavigator . stateContext . data ?? { } ;
421+ if ( historyAction === 'replace' ) {
422+ selectionStackRef . current . pop ( ) ;
423+ }
347424 mainNavigator . refresh (
348425 { ...currentData , [ selectionParam as string ] : href } ,
349- 'add'
426+ historyAction === 'replace' ? 'replace' : 'add'
350427 ) ;
351428 } ;
352429 return new Proxy ( detailNavigator , {
@@ -357,6 +434,9 @@ function WideSplitView({
357434 : ( key : string , params ?: any ) =>
358435 detailNavigator . navigate ( key , params ) ;
359436 }
437+ if ( selectionParam && prop === 'navigateLink' ) {
438+ return navigateLinkMirroringUrl ;
439+ }
360440 if ( selectionParam && prop === 'navigateBack' ) {
361441 return paneGoBack ;
362442 }
@@ -372,6 +452,7 @@ function WideSplitView({
372452 selectionParam ,
373453 mainNavigator ,
374454 encodeSelectionHref ,
455+ navigateLinkMirroringUrl ,
375456 paneGoBack ,
376457 paneCanGoBack ,
377458 ] ) ;
@@ -385,7 +466,6 @@ function WideSplitView({
385466 // Master taps REPLACE the pane (fluent reset → one screen). Detail drills
386467 // STACK (navigate on top of the existing crumb trail) so screens sit on top
387468 // of each other — iOS Settings style — while still being URL-history-backed.
388- const appliedHrefRef = React . useRef < string | undefined > ( undefined ) ;
389469 React . useEffect ( ( ) => {
390470 if ( ! selectionParam ) {
391471 return undefined ;
0 commit comments