feat(ui): add close buttons on panels and Add Panel block (#1354)

* feat(ui): add close buttons on panels and Add Panel block

Add hover-visible close (×) buttons to panel headers that disable the
panel via the existing toggle infrastructure, and an "Add Panel" card
at the end of the grid that opens the Settings → Panels tab.

- Close button on all panels except Live News and Live Webcams
- Button always positioned far-right via CSS order: 999
- Panel count badges and action buttons pushed right with margin-left: auto
- World Clock gear icon shifted to avoid overlap with close button
- Styled icon-btn class for Airline Intelligence refresh button
- i18n keys added for closePanel and addPanel
- wm:panel-close custom event handled in event-handlers.ts

Closes #1347

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add PR screenshots for panel controls feature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): address PR review — move inline styles to CSS, store event listener ref

- Move inline marginLeft from MarketPanel and AirlineIntelPanel to CSS
- Store wm:panel-close listener as boundPanelCloseHandler with cleanup in destroy()
- Close button now extends .icon-btn (shared base styles, 5 overrides instead of 15)
- Scope .live-news-settings-btn margin-left to .panel-header context only
- Add gap: 8px to .panel-header for uniform spacing
- Center LIVE badge and sparkle btn between title and count/close via auto margins
- Fix close button hover/touch specificity by scoping to .panel-header

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ui): consolidate margin-left auto, fix close btn icon and hover color

- Replace scattered margin-left: auto with single .panel-header-left + *
  selector to correctly push the first right-aligned element
- Use multiplication X (U+2715) instead of multiplication sign (U+00D7)
  for the close button icon
- Use color-mix with --semantic-critical for close hover background
  instead of hardcoded rgba
- Convert wc-settings-btn from absolute positioning to flex flow,
  removing the fragile right: 30px magic number

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
Nicolas Dos Santos
2026-03-10 00:41:48 -07:00
committed by GitHub
parent 67a7da8b4d
commit fa36b5d37c
10 changed files with 154 additions and 8 deletions

BIN
.github/screenshots/add-panel-block.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

BIN
.github/screenshots/close-buttons.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -85,6 +85,7 @@ export class EventHandlerManager implements AppModule {
private boundMapResizeVisChangeHandler: (() => void) | null = null;
private boundMapFullscreenEscHandler: ((e: KeyboardEvent) => void) | null = null;
private boundMobileMenuKeyHandler: ((e: KeyboardEvent) => void) | null = null;
private boundPanelCloseHandler: ((e: Event) => void) | null = null;
private idleTimeoutId: ReturnType<typeof setTimeout> | null = null;
private snapshotIntervalId: ReturnType<typeof setInterval> | null = null;
private clockIntervalId: ReturnType<typeof setInterval> | null = null;
@@ -230,6 +231,10 @@ export class EventHandlerManager implements AppModule {
document.removeEventListener('keydown', this.boundMobileMenuKeyHandler);
this.boundMobileMenuKeyHandler = null;
}
if (this.boundPanelCloseHandler) {
this.ctx.container.removeEventListener('wm:panel-close', this.boundPanelCloseHandler);
this.boundPanelCloseHandler = null;
}
this.ctx.tvMode?.destroy();
this.ctx.tvMode = null;
this.ctx.unifiedSettings?.destroy();
@@ -277,6 +282,19 @@ export class EventHandlerManager implements AppModule {
};
window.addEventListener('storage', this.boundStorageHandler);
// Handle panel close (X) button clicks
this.boundPanelCloseHandler = ((e: CustomEvent<{ panelId: string }>) => {
const { panelId } = e.detail;
const config = this.ctx.panelSettings[panelId];
if (!config) return;
config.enabled = false;
trackPanelToggled(panelId, false);
saveToStorage(STORAGE_KEYS.panels, this.ctx.panelSettings);
this.applyPanelSettings();
this.ctx.unifiedSettings?.refreshPanelToggles();
}) as EventListener;
this.ctx.container.addEventListener('wm:panel-close', this.boundPanelCloseHandler);
document.getElementById('headerThemeToggle')?.addEventListener('click', () => {
const next = getCurrentTheme() === 'dark' ? 'light' : 'dark';
setTheme(next);

View File

@@ -886,6 +886,23 @@ export class PanelLayoutManager implements AppModule {
}
});
// "+" Add Panel block at the end of the grid
const addPanelBlock = document.createElement('button');
addPanelBlock.className = 'add-panel-block';
addPanelBlock.setAttribute('aria-label', t('components.panel.addPanel'));
const addIcon = document.createElement('span');
addIcon.className = 'add-panel-block-icon';
addIcon.textContent = '+';
const addLabel = document.createElement('span');
addLabel.className = 'add-panel-block-label';
addLabel.textContent = t('components.panel.addPanel');
addPanelBlock.appendChild(addIcon);
addPanelBlock.appendChild(addLabel);
addPanelBlock.addEventListener('click', () => {
this.ctx.unifiedSettings?.open('panels');
});
panelsGrid.appendChild(addPanelBlock);
const bottomGrid = document.getElementById('mapBottomGrid');
if (bottomGrid) {
bottomOrder.forEach(key => {

View File

@@ -363,7 +363,7 @@ export class LiveNewsPanel extends Panel {
private idleCallbackId: number | ReturnType<typeof setTimeout> | null = null;
constructor() {
super({ id: 'live-news', title: t('panels.liveNews'), className: 'panel-wide' });
super({ id: 'live-news', title: t('panels.liveNews'), className: 'panel-wide', closable: false });
this.insertLiveCountBadge(OPTIONAL_LIVE_CHANNELS.length);
this.youtubeOrigin = LiveNewsPanel.resolveYouTubeOrigin();
this.playerElementId = `live-news-player-${Date.now()}`;

View File

@@ -99,7 +99,7 @@ export class LiveWebcamsPanel extends Panel {
private boundEmbedMessageHandler: (e: MessageEvent) => void;
constructor() {
super({ id: 'live-webcams', title: t('panels.liveWebcams'), className: 'panel-wide' });
super({ id: 'live-webcams', title: t('panels.liveWebcams'), className: 'panel-wide', closable: false });
this.insertLiveCountBadge(WEBCAM_FEEDS.length);
// Mobile: force single-cam view. 4 iframes at once is a battery + performance disaster.

View File

@@ -14,6 +14,7 @@ export interface PanelOptions {
trackActivity?: boolean;
infoTooltip?: string;
premium?: 'locked' | 'enhanced';
closable?: boolean;
}
const PANEL_SPANS_KEY = 'worldmonitor-panel-spans';
@@ -267,6 +268,10 @@ export class Panel {
this.header.appendChild(this.countEl);
}
if (options.closable !== false) {
this.appendCloseButton();
}
this.content = document.createElement('div');
this.content.className = 'panel-content';
this.content.id = `${options.id}Content`;
@@ -642,6 +647,22 @@ export class Panel {
headerLeft.appendChild(badge);
}
protected appendCloseButton(): void {
const closeBtn = h('button', {
className: 'icon-btn panel-close-btn',
'aria-label': t('components.panel.closePanel'),
title: t('components.panel.closePanel'),
}, '\u2715');
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.element.dispatchEvent(new CustomEvent('wm:panel-close', {
bubbles: true,
detail: { panelId: this.panelId },
}));
});
this.header.appendChild(closeBtn);
}
public getElement(): HTMLElement {
return this.element;
}

View File

@@ -1618,7 +1618,9 @@
"panel": {
"showMethodologyInfo": "Show methodology info",
"dragToResize": "Drag to resize (double-click to reset)",
"openSettings": "Open Settings"
"openSettings": "Open Settings",
"closePanel": "Close panel",
"addPanel": "Add Panel"
},
"languageSelector": {
"selectLanguage": "Select Language",

View File

@@ -1238,6 +1238,7 @@ body.panel-resize-active iframe {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 10px;
background: var(--overlay-subtle);
border-bottom: 1px solid var(--border);
@@ -1286,6 +1287,11 @@ body.panel-resize-active iframe {
color: var(--text);
}
/* Push the first element after header-left to the right; subsequent siblings flow via gap */
.panel-header > .panel-header-left + * {
margin-left: auto;
}
.panel-count {
font-size: 10px;
color: var(--text-dim);
@@ -1320,6 +1326,91 @@ body.panel-resize-active iframe {
}
/* ---- Icon buttons in panel headers ---- */
.panel-header .icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 3px;
background: transparent;
color: var(--text-dim);
font-size: 13px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
flex-shrink: 0;
}
.panel-header .icon-btn:hover {
background: var(--overlay-subtle);
color: var(--text);
}
/* ---- Close (X) button on panels (extends .icon-btn) ---- */
.panel-header .panel-close-btn {
font-size: 14px;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
order: 999;
}
.panel:hover .panel-close-btn,
.panel-close-btn:focus-visible {
opacity: 1;
}
.panel-header .panel-close-btn:hover {
background: color-mix(in srgb, var(--semantic-critical) 15%, transparent);
color: var(--semantic-critical);
}
/* On touch devices, always show the close button */
@media (hover: none) {
.panel-header .panel-close-btn {
opacity: 0.7;
}
}
/* ---- Add Panel (+) block ---- */
.add-panel-block {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
min-height: 120px;
border: 2px dashed var(--border);
border-radius: var(--panel-radius, 6px);
background: transparent;
color: var(--text-dim);
cursor: pointer;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
padding: 0;
font-family: inherit;
}
.add-panel-block:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(0, 255, 136, 0.04);
}
.add-panel-block-icon {
font-size: 28px;
line-height: 1;
}
.add-panel-block-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.panel-data-badge {
font-size: 9px;
letter-spacing: 0.4px;
@@ -1384,7 +1475,6 @@ body.panel-resize-active iframe {
cursor: pointer;
font-size: 11px;
padding: 2px 6px;
margin-right: 6px;
opacity: 0.85;
transition: opacity 0.15s, transform 0.15s, background 0.15s;
}
@@ -1676,6 +1766,7 @@ body.panel-resize-active iframe {
color: var(--text);
}
/* Channel management list: same layout as LIVE panel channel switcher */
.live-news-manage-list {
display: flex;

View File

@@ -1489,10 +1489,6 @@
World Clock Panel
---------------------------------------------------------- */
.wc-settings-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-dim);
@@ -1502,6 +1498,7 @@
line-height: 1;
opacity: 0.6;
transition: opacity 0.15s;
flex-shrink: 0;
}
.wc-settings-btn:hover,