@@ -163,6 +163,17 @@ function insertColumns(baseIndex: number, position: 'left' | 'right', count: num
163163}
164164
165165// ── Custom context menu ───────────────────────────────────────────────────────
166+ function makeRowItem ( label : string , iconClass : string , danger = false ) : HTMLDivElement {
167+ const item = document . createElement ( 'div' ) ;
168+ item . className = 'row-ctx-item' + ( danger ? ' danger' : '' ) ;
169+ const icon = document . createElement ( 'i' ) ;
170+ icon . className = 'codicon ' + iconClass ;
171+ const span = document . createElement ( 'span' ) ;
172+ span . className = 'row-ctx-label' ;
173+ span . textContent = label ;
174+ item . append ( icon , span ) ;
175+ return item ;
176+ }
166177
167178function hideMenu ( ) : void {
168179 document . getElementById ( 'row-context-menu' ) ?. classList . add ( 'hidden' ) ;
@@ -185,15 +196,11 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
185196 // ── Copy ──────────────────────────────────────────────────────────────────
186197 if ( hasMultiSelection ( ) ) {
187198 // A range is selected — offer range copy, with and without header row.
188- const copyRange = document . createElement ( 'div' ) ;
189- copyRange . className = 'row-ctx-item' ;
190- copyRange . textContent = 'Copy' ;
199+ const copyRange = makeRowItem ( 'Copy' , 'codicon-copy' ) ;
191200 copyRange . addEventListener ( 'click' , ( ) => { copySelection ( false ) ; hideMenu ( ) ; } ) ;
192201 menu . appendChild ( copyRange ) ;
193202
194- const copyWithHeader = document . createElement ( 'div' ) ;
195- copyWithHeader . className = 'row-ctx-item' ;
196- copyWithHeader . textContent = 'Copy with header' ;
203+ const copyWithHeader = makeRowItem ( 'Copy with header' , 'codicon-copy' ) ;
197204 copyWithHeader . addEventListener ( 'click' , ( ) => { copySelection ( true ) ; hideMenu ( ) ; } ) ;
198205 menu . appendChild ( copyWithHeader ) ;
199206
@@ -207,9 +214,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
207214 const raw = node ?. data ?. [ colId ] ;
208215 const value = raw != null ? String ( raw ) : '' ;
209216
210- const copyItem = document . createElement ( 'div' ) ;
211- copyItem . className = 'row-ctx-item' ;
212- copyItem . textContent = 'Copy' ;
217+ const copyItem = makeRowItem ( 'Copy' , 'codicon-copy' ) ;
213218 copyItem . addEventListener ( 'click' , ( ) => {
214219 navigator . clipboard . writeText ( value ) . catch ( ( ) => { } ) ;
215220 hideMenu ( ) ;
@@ -227,30 +232,13 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
227232 // the two are mutually exclusive (runDetect() drops any frozen row).
228233 if ( state . dupRowSet . size === 0 && ! state . dupShowOnly ) {
229234 if ( isPinnedRow ) {
230- // Right-clicked a pinned (frozen) row. pinnedOrig is resolved from the
231- // DOM by the contextmenu handler. Only show the per-row item when it
232- // resolved, so an unresolved click can NEVER fall back to clearing all
233- // freezes. "Unfreeze all rows" is a separate, explicit action.
235+ // Right-clicked a pinned (frozen) row -> unfreeze just that row.
234236 const clickedOrig = pinnedOrig ;
235237 if ( clickedOrig != null ) {
236- const item = document . createElement ( 'div' ) ;
237- item . className = 'row-ctx-item' ;
238- item . textContent = '📌 Unfreeze row' ;
238+ const item = makeRowItem ( 'Unfreeze row' , 'codicon-pin' ) ;
239239 item . addEventListener ( 'click' , ( ) => { unfreezeRow ( clickedOrig ) ; hideMenu ( ) ; } ) ;
240240 menu . appendChild ( item ) ;
241241 }
242- if ( frozenRowCount ( ) > 1 ) {
243- const all = document . createElement ( 'div' ) ;
244- all . className = 'row-ctx-item' ;
245- all . textContent = '📌 Unfreeze all rows' ;
246- all . addEventListener ( 'click' , ( ) => { unfreezeAllRows ( ) ; hideMenu ( ) ; } ) ;
247- menu . appendChild ( all ) ;
248- }
249- if ( clickedOrig != null || frozenRowCount ( ) > 1 ) {
250- const sep = document . createElement ( 'div' ) ;
251- sep . className = 'col-ctx-separator' ;
252- menu . appendChild ( sep ) ;
253- }
254242 } else if ( rowIndex !== null ) {
255243 // Body rows are never frozen themselves (a frozen row moves to the
256244 // pinned band), so the body menu only offers Freeze. With a multi-row
@@ -265,17 +253,27 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
265253 }
266254
267255 if ( origs . length > 0 ) {
268- const item = document . createElement ( 'div' ) ;
269- item . className = 'row-ctx-item' ;
270- item . textContent = origs . length > 1 ? `📌 Freeze ${ origs . length } rows` : '📌 Freeze row' ;
256+ const item = makeRowItem ( origs . length > 1 ? `Freeze ${ origs . length } rows` : 'Freeze row' , 'codicon-pinned' ) ;
271257 item . addEventListener ( 'click' , ( ) => { freezeRows ( origs ) ; hideMenu ( ) ; } ) ;
272258 menu . appendChild ( item ) ;
273-
274- const sep = document . createElement ( 'div' ) ;
275- sep . className = 'col-ctx-separator' ;
276- menu . appendChild ( sep ) ;
277259 }
278260 }
261+
262+ // "Unfreeze all rows (N)" sits below the per-row items, shown on any row
263+ // while more than one row is frozen - mirrors "Unfreeze all columns".
264+ if ( frozenRowCount ( ) > 1 ) {
265+ const all = makeRowItem ( `Unfreeze all rows (${ frozenRowCount ( ) } )` , 'codicon-pin' ) ;
266+ all . addEventListener ( 'click' , ( ) => { unfreezeAllRows ( ) ; hideMenu ( ) ; } ) ;
267+ menu . appendChild ( all ) ;
268+ }
269+
270+ // One separator for the freeze group, if it has any item.
271+ const hasFreezeItem = ( isPinnedRow && pinnedOrig != null ) || ( ! isPinnedRow && rowIndex !== null ) || frozenRowCount ( ) > 1 ;
272+ if ( hasFreezeItem ) {
273+ const sep = document . createElement ( 'div' ) ;
274+ sep . className = 'col-ctx-separator' ;
275+ menu . appendChild ( sep ) ;
276+ }
279277 }
280278
281279 // ── Insert row(s) above/below ─────────────────────────────────────────────
@@ -289,18 +287,14 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
289287 const topEdge = inSel ? Math . min ( ...selectedRows ) : rowIndex ;
290288 const bottomEdge = inSel ? Math . max ( ...selectedRows ) : rowIndex ;
291289
292- const insertAbove = document . createElement ( 'div' ) ;
293- insertAbove . className = 'row-ctx-item' ;
294- insertAbove . textContent = count > 1 ? `Insert ${ count } rows above` : 'Insert row above' ;
290+ const insertAbove = makeRowItem ( count > 1 ? `Insert ${ count } rows above` : 'Insert row above' , 'codicon-arrow-up' ) ;
295291 insertAbove . addEventListener ( 'click' , ( ) => {
296292 insertRows ( topEdge , 'above' , count ) ;
297293 hideMenu ( ) ;
298294 } ) ;
299295 menu . appendChild ( insertAbove ) ;
300296
301- const insertBelow = document . createElement ( 'div' ) ;
302- insertBelow . className = 'row-ctx-item' ;
303- insertBelow . textContent = count > 1 ? `Insert ${ count } rows below` : 'Insert row below' ;
297+ const insertBelow = makeRowItem ( count > 1 ? `Insert ${ count } rows below` : 'Insert row below' , 'codicon-arrow-down' ) ;
304298 insertBelow . addEventListener ( 'click' , ( ) => {
305299 insertRows ( bottomEdge , 'below' , count ) ;
306300 hideMenu ( ) ;
@@ -323,10 +317,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
323317 ? selectedRows
324318 : [ rowIndex ] ;
325319
326- const label = rowIndices . length > 1 ? `Delete ${ rowIndices . length } rows` : 'Delete row' ;
327- const delRowItem = document . createElement ( 'div' ) ;
328- delRowItem . className = 'row-ctx-item danger' ;
329- delRowItem . textContent = label ;
320+ const delRowItem = makeRowItem ( rowIndices . length > 1 ? `Delete ${ rowIndices . length } rows` : 'Delete row' , 'codicon-trash' , true ) ;
330321 delRowItem . addEventListener ( 'click' , ( ) => {
331322 deleteRows ( rowIndices ) ;
332323 hideMenu ( ) ;
@@ -340,9 +331,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
340331 const selectedCols = getSelectedColIndices ( ) ;
341332 const useMulti = selectedCols . length > 1 && selectedCols . includes ( colIndex ) ;
342333
343- const delColItem = document . createElement ( 'div' ) ;
344- delColItem . className = 'row-ctx-item danger' ;
345- delColItem . textContent = useMulti ? `Delete ${ selectedCols . length } columns` : 'Delete column' ;
334+ const delColItem = makeRowItem ( useMulti ? `Delete ${ selectedCols . length } columns` : 'Delete column' , 'codicon-trash' , true ) ;
346335 delColItem . addEventListener ( 'click' , ( ) => {
347336 if ( useMulti ) deleteColumns ( selectedCols ) ;
348337 else if ( colId ) deleteColumn ( colId ) ;
@@ -351,6 +340,18 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
351340 menu . appendChild ( delColItem ) ;
352341 }
353342
343+ // Remove orphan separators: drop a leading/trailing separator and any two
344+ // that ended up adjacent (e.g. a middle group rendered nothing).
345+ const items = Array . from ( menu . children ) ;
346+ let lastWasSep = true ;
347+ for ( const el of items ) {
348+ const isSep = el . classList . contains ( 'col-ctx-separator' ) ;
349+ if ( isSep && lastWasSep ) { el . remove ( ) ; continue ; }
350+ lastWasSep = isSep ;
351+ }
352+ const last = menu . lastElementChild ;
353+ if ( last ?. classList . contains ( 'col-ctx-separator' ) ) last . remove ( ) ;
354+
354355 if ( menu . children . length === 0 ) return ;
355356
356357 // Position — keep menu on screen
@@ -431,8 +432,10 @@ export function setupDeleteRowCol(): void {
431432 const rowIndex = riStr != null ? parseInt ( riStr , 10 ) : null ;
432433
433434 // For a pinned (frozen) row, resolve which row it is independently of AG
434- // Grid's pinned-row index scheme: the '#' gutter cell renders "📌<origIndex>"
435- // (builder.ts valueGetter), so read that from the matching row in the band.
435+ // Grid's pinned-row index scheme: the '#' gutter cell renders a pin icon +
436+ // the origIndex (builder.ts cellRenderer). The icon is a ::before glyph with
437+ // no text node, so the cell's textContent is just the number - read it from
438+ // the matching row in the band.
436439 let pinnedOrig : number | null = null ;
437440 if ( isPinnedRow && agRow && riStr != null ) {
438441 const ft = agRow . closest ( '.ag-floating-top' ) ;
0 commit comments