11import Base from '../core/Base.mjs' ;
2+ import DockRail from './DockRail.mjs' ;
23import DockSplitter from './DockSplitter.mjs' ;
34import DockTabSortZone from './DockTabSortZone.mjs' ;
45import DockZoneModel from './DockZoneModel.mjs' ;
@@ -208,6 +209,8 @@ class DockLayoutAdapter extends Base {
208209
209210 return this . projectNode ( model . root , {
210211 applyDockZoneOperation : options . applyDockZoneOperation ,
212+ autoHideRevealOnHover : options . autoHideRevealOnHover === true ,
213+ defaultRevealFraction : Number . isFinite ( options . defaultRevealFraction ) ? options . defaultRevealFraction : null ,
211214 dockZoneDocument : options . dockZoneDocument || model ,
212215 items : model . items || { } ,
213216 nodes : model . nodes ,
@@ -257,10 +260,12 @@ class DockLayoutAdapter extends Base {
257260 }
258261
259262 /**
260- * Projects one auto-hidden item id into a collapsed rail-tab affordance .
263+ * Projects one auto-hidden item id into the rail-tab metadata `DockRail` renders .
261264 *
262- * The rail tab carries stable `dockItemId` + `dockEdge` metadata so a later reveal/pin slice
263- * can convert a click into a transient reveal or a `setItemPinned` operation.
265+ * The metadata carries stable `dockItemId` + `dockEdge` so the rail's click (and the follow-up
266+ * reveal/pin slice) can address the item semantically, plus the `restorable` policy projection
267+ * (`pinnable !== false`) — a tab whose restore the model would reject renders disabled instead
268+ * of lying about the affordance.
264269 * No DOMRect, hover, or open geometry is emitted — reveal/open state stays runtime-only per the
265270 * JSON-first guardrail (HarnessDockZoneModel.md §Serializable vs Runtime).
266271 * @param {String } itemId
@@ -274,18 +279,21 @@ class DockLayoutAdapter extends Base {
274279 let item = context . items [ itemId ] || { } ;
275280
276281 return {
277- cls : [ 'neo-dashboard-dock-rail-tab' ] ,
278- data : { dockEdge : edge , dockItemId : itemId , dockRailTab : true } ,
279- dockEdge : edge ,
280- dockItemId : itemId ,
281- dockNodeType : 'edge-rail-tab' ,
282- ntype : 'button' ,
283- text : item . title || itemId
282+ dockEdge : edge ,
283+ dockItemId : itemId ,
284+ restorable : item . pinnable !== false ,
285+ title : item . title || itemId
284286 }
285287 }
286288
287289 /**
288- * Projects a set of auto-hidden item ids into a thin edge-rail strip for the owning edge.
290+ * Projects a set of auto-hidden item ids into a `Neo.dashboard.DockRail` affordance for the
291+ * owning edge.
292+ *
293+ * Mirrors `createSplitterAffordance()`: the reducer callbacks thread from projection context into
294+ * the component so restore commits ride the workspace's single operation path — no parallel
295+ * mutation grammar. Rail extent and tab writing-mode are CSS concerns keyed off the per-edge cls
296+ * hook (JSON-first: no pixel geometry in the projection).
289297 * @param {String[] } itemIds
290298 * @param {String } edge One of `top`, `right`, `bottom`, `left`.
291299 * @param {Object } context
@@ -294,16 +302,75 @@ class DockLayoutAdapter extends Base {
294302 * @static
295303 */
296304 static createEdgeRail ( itemIds , edge , context ) {
297- let isVertical = edge === 'left' || edge === 'right' ;
298-
299305 return {
300- cls : [ 'neo-dashboard-dock-edge-rail' , `neo-dashboard-dock-edge-rail-${ edge } ` ] ,
301- dockEdge : edge ,
302- dockNodeType : 'edge-rail' ,
303- items : itemIds . map ( itemId => this . createRailTab ( itemId , edge , context ) ) ,
304- layout : { ntype : isVertical ? 'vbox' : 'hbox' , align : 'start' } ,
305- ntype : 'container'
306+ applyDockZoneOperation : context . applyDockZoneOperation ,
307+ autoHideRevealOnHover : context . autoHideRevealOnHover === true ,
308+ defaultRevealFraction : context . defaultRevealFraction ?? null ,
309+ dockEdge : edge ,
310+ dockNodeType : 'edge-rail' ,
311+ dockZoneDocument : context . dockZoneDocument ,
312+ edge,
313+ module : DockRail ,
314+ ntype : 'dashboard-dock-rail' ,
315+ onDockZoneDocumentChange : context . onDockZoneDocumentChange ,
316+ railItems : itemIds . map ( itemId => this . createRailTab ( itemId , edge , context ) ) ,
317+ resolveComponentRef : context . resolveComponentRef
318+ }
319+ }
320+
321+ /**
322+ * Resolves the reveal overlay's free-dimension extent for an item: the share its owning
323+ * subtree holds at the nearest ancestor split, per that split's committed `sizes` — the
324+ * "last committed extent, still in the document" rule. Returns `null` when no ancestor split
325+ * carries usable sizes (e.g. a tabs node sitting directly in an edge-zone slot); the overlay
326+ * then falls back to its workspace-configurable default fraction. Never reads DOM geometry.
327+ * @param {Object } model Committed dock-zone document.
328+ * @param {String } itemId
329+ * @returns {Number|null } Fraction in `(0, 1]`, or `null`.
330+ * @static
331+ */
332+ static resolveRevealExtent ( model , itemId ) {
333+ let nodes = model ?. nodes || { } ,
334+ childId = Object . keys ( nodes ) . find ( nodeId =>
335+ nodes [ nodeId ] . type === 'tabs' && ( nodes [ nodeId ] . items || [ ] ) . includes ( itemId ) ) ,
336+ parent ;
337+
338+ const findParent = id => {
339+ for ( const [ nodeId , node ] of Object . entries ( nodes ) ) {
340+ if ( node . type === 'split' && ( node . children || [ ] ) . includes ( id ) ) {
341+ return { index : node . children . indexOf ( id ) , nodeId, split : true }
342+ }
343+ if ( node . type === 'edge-zone' && Object . values ( node . zones || { } ) . includes ( id ) ) {
344+ return { nodeId, split : false }
345+ }
346+ }
347+ return null
348+ } ;
349+
350+ while ( childId ) {
351+ parent = findParent ( childId ) ;
352+
353+ if ( ! parent ) {
354+ return null
355+ }
356+
357+ if ( parent . split ) {
358+ let sizes = nodes [ parent . nodeId ] . sizes ;
359+
360+ if ( Array . isArray ( sizes ) && sizes . length ) {
361+ let total = sizes . reduce ( ( sum , value ) => sum + ( Number ( value ) || 0 ) , 0 ) ,
362+ share = Number ( sizes [ parent . index ] ) ;
363+
364+ return total > 0 && Number . isFinite ( share ) && share > 0 ? share / total : null
365+ }
366+
367+ return null
368+ }
369+
370+ childId = parent . nodeId
306371 }
372+
373+ return null
307374 }
308375
309376 /**
@@ -340,20 +407,27 @@ class DockLayoutAdapter extends Base {
340407 // Pass the railed set down so projectTabsNode drops those items from the tab flow.
341408 let childContext = railedItemIds . size ? { ...context , railedItemIds} : context ,
342409 middleItems = [ ] ,
343- rows = [ ] ;
410+ rows = [ ] ,
411+ centerConfig ;
344412
345413 if ( railsByEdge . left ) middleItems . push ( this . createEdgeRail ( railsByEdge . left , 'left' , context ) ) ;
346- if ( zones . left ) middleItems . push ( this . projectNode ( zones . left , childContext ) ) ;
347- if ( zones . center ) middleItems . push ( this . projectNode ( zones . center , childContext ) ) ;
348- if ( zones . right ) middleItems . push ( this . projectNode ( zones . right , childContext ) ) ;
414+ if ( zones . left ) middleItems . push ( this . projectEdgeBand ( zones . left , 'left' , childContext ) ) ;
415+
416+ if ( zones . center ) {
417+ centerConfig = this . projectNode ( zones . center , childContext ) ;
418+ centerConfig . flex = 1 ;
419+ middleItems . push ( centerConfig )
420+ }
421+
422+ if ( zones . right ) middleItems . push ( this . projectEdgeBand ( zones . right , 'right' , childContext ) ) ;
349423 if ( railsByEdge . right ) middleItems . push ( this . createEdgeRail ( railsByEdge . right , 'right' , context ) ) ;
350424
351425 if ( railsByEdge . top ) {
352426 rows . push ( this . createEdgeRail ( railsByEdge . top , 'top' , context ) )
353427 }
354428
355429 if ( zones . top ) {
356- rows . push ( this . projectNode ( zones . top , childContext ) )
430+ rows . push ( this . projectEdgeBand ( zones . top , 'top' , childContext ) )
357431 }
358432
359433 if ( middleItems . length === 1 ) {
@@ -369,7 +443,7 @@ class DockLayoutAdapter extends Base {
369443 }
370444
371445 if ( zones . bottom ) {
372- rows . push ( this . projectNode ( zones . bottom , childContext ) )
446+ rows . push ( this . projectEdgeBand ( zones . bottom , 'bottom' , childContext ) )
373447 }
374448
375449 if ( railsByEdge . bottom ) {
@@ -386,6 +460,26 @@ class DockLayoutAdapter extends Base {
386460 }
387461 }
388462
463+ /**
464+ * Marks an edge-band zone projection: bands keep a fixed cross-extent (the
465+ * `neo-dashboard-dock-edge-band(-edge)` CSS hooks) instead of flexing against the center —
466+ * an unsized band silently eats workspace geometry the center owns.
467+ * @param {String } zoneId
468+ * @param {String } edge One of `top`, `right`, `bottom`, `left`.
469+ * @param {Object } context
470+ * @returns {Object }
471+ * @protected
472+ * @static
473+ */
474+ static projectEdgeBand ( zoneId , edge , context ) {
475+ let config = this . projectNode ( zoneId , context ) ;
476+
477+ config . cls = [ ...( config . cls || [ ] ) , 'neo-dashboard-dock-edge-band' , `neo-dashboard-dock-edge-band-${ edge } ` ] ;
478+ config . flex = 'none' ;
479+
480+ return config
481+ }
482+
389483 /**
390484 * Projects one dock item id into a Neo config or recoverable placeholder.
391485 * @param {String } itemId
0 commit comments