@@ -104,30 +104,43 @@ export function DetailPage({
104104
105105 return (
106106 < div className = "space-y-4" >
107- { /* Header (#572): the title is the page's most important element
108- and gets as much horizontal space as it needs (`flex-1
109- min-w-0`); the toolbar is `shrink-0` and only pushes the title
110- when it genuinely can't fit on its row. `justify-end` on the
111- toolbar's flex-wrap keeps wrapped button rows flush right to
112- the page padding, instead of left-aligned within their column. */ }
113- < header className = "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4" >
114- < div className = "min-w-0 flex-1 space-y-1" >
115- < Breadcrumb
116- items = { [
117- { label : 'Home' , to : '/' } ,
118- { label : modelPlural , to : listPath } ,
119- { label : data . label } ,
120- ] }
121- renderLink = { ( to , className , label ) => (
122- < Link to = { to } className = { className } >
123- { label }
124- </ Link >
125- ) }
126- />
127- < h1 className = "text-2xl font-semibold" > { data . label } </ h1 >
128- </ div >
107+ { /* Header (#572 / #658): three stacked full-width rows so the
108+ title never shares horizontal space with the toolbar — the
109+ old side-by-side layout collapsed long single-token titles
110+ (filenames, slugs, UUIDs) into one-word-per-line at full H1
111+ size, AND let an 8+ action toolbar push the title clean off
112+ the viewport. Each concern now owns its own row:
113+
114+ row 1: breadcrumb (full width, truncates on tight viewports)
115+ row 2: H1 (full width, `overflow-wrap: anywhere` so a
116+ single long token wraps inside the container)
117+ row 3: toolbar (full width, `flex-wrap` so 8+ actions flow
118+ to new lines; primary actions Edit/Delete
119+ sit at the trailing edge via `ml-auto`)
120+
121+ NOTE: re-applied after the #657 module-split refactor silently
122+ reverted it to the pre-#658 single-row layout. */ }
123+ < header className = "space-y-2" >
124+ < Breadcrumb
125+ items = { [
126+ { label : 'Home' , to : '/' } ,
127+ { label : modelPlural , to : listPath } ,
128+ { label : data . label } ,
129+ ] }
130+ renderLink = { ( to , className , label ) => (
131+ < Link to = { to } className = { className } >
132+ { label }
133+ </ Link >
134+ ) }
135+ />
136+ { /* `break-words` (Tailwind's overflow-wrap: anywhere) keeps a
137+ very long single-token title wrapping inside the container
138+ at H1 size, instead of forcing each hyphen segment onto its
139+ own line. `text-balance` rebalances the wrap for shorter
140+ multi-word titles too. */ }
141+ < h1 className = "text-2xl font-semibold text-balance break-words" > { data . label } </ h1 >
129142 { ! editing && (
130- < div className = "flex shrink-0 flex-wrap justify-end gap-2" >
143+ < div className = "flex flex-wrap items-center gap-2" >
131144 < button
132145 type = "button"
133146 onClick = { ( ) => setHistoryOpen ( true ) }
@@ -210,33 +223,41 @@ export function DetailPage({
210223 onError = { ( message ) => toast . error ( message ) }
211224 />
212225 ) ) }
213- { /* Refresh (#592): refetch the object + inlines + history
214- with no full page reload. Placed between the actions
215- cluster and the Edit / Delete pair so destructive
216- buttons stay at the trailing edge. */ }
217- < RefreshButton
218- onRefresh = { refresh }
219- tooltip = "Refresh"
220- icon = { < RefreshCw className = "h-4 w-4" aria-hidden /> }
221- />
222- { canChange && (
223- < Button variant = "primary" onClick = { ( ) => setEditing ( true ) } >
224- < span className = "inline-flex items-center gap-1.5" >
225- < Pencil className = "h-4 w-4" aria-hidden /> Edit
226- </ span >
227- </ Button >
228- ) }
229- { canDelete && (
230- < DeleteButton
231- label = { data . label }
232- loadPreview = { ( ) => fetchDeletePreview ( { client, appLabel, modelName, pk } ) }
233- onConfirm = { async ( ) => {
234- await deleteObject ( { client, appLabel, modelName, pk } ) ;
235- toast . success ( `Deleted “${ data . label } ”.` ) ;
236- navigate ( listPath ) ;
237- } }
226+ { /* Primary-actions cluster (Refresh + Edit + Delete) is
227+ grouped with `ml-auto` so it floats to the trailing
228+ edge of the toolbar row even when the leading
229+ `@admin.action` cluster wraps onto multiple lines.
230+ `flex-wrap` on the cluster itself keeps Edit / Delete
231+ together if the row is too narrow for the whole group
232+ — destructive Delete stays adjacent to the constructive
233+ Edit, never orphaned. */ }
234+ < div className = "ml-auto flex flex-wrap items-center gap-2" >
235+ { /* Refresh (#592): refetch the object + inlines + history
236+ with no full page reload. */ }
237+ < RefreshButton
238+ onRefresh = { refresh }
239+ tooltip = "Refresh"
240+ icon = { < RefreshCw className = "h-4 w-4" aria-hidden /> }
238241 />
239- ) }
242+ { canChange && (
243+ < Button variant = "primary" onClick = { ( ) => setEditing ( true ) } >
244+ < span className = "inline-flex items-center gap-1.5" >
245+ < Pencil className = "h-4 w-4" aria-hidden /> Edit
246+ </ span >
247+ </ Button >
248+ ) }
249+ { canDelete && (
250+ < DeleteButton
251+ label = { data . label }
252+ loadPreview = { ( ) => fetchDeletePreview ( { client, appLabel, modelName, pk } ) }
253+ onConfirm = { async ( ) => {
254+ await deleteObject ( { client, appLabel, modelName, pk } ) ;
255+ toast . success ( `Deleted “${ data . label } ”.` ) ;
256+ navigate ( listPath ) ;
257+ } }
258+ />
259+ ) }
260+ </ div >
240261 </ div >
241262 ) }
242263 </ header >
0 commit comments