mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
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:
committed by
GitHub
parent
67a7da8b4d
commit
fa36b5d37c
BIN
.github/screenshots/add-panel-block.png
vendored
Normal file
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
BIN
.github/screenshots/close-buttons.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 KiB |
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user