Skip to content

Commit 34aa310

Browse files
rayanhabib07claudekoala73
authored
Feat/drag system 2217 (koala73#2444)
* feat(panels): improve drag UX and add close buttons to live panels * fix: address PR feedback from koala73 and SebastienMelki * fix: replace unsafe any cast with type guard for fetchData Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(panels): add collapsible support to live news and webcams panels - Add collapsible option to Panel base class with collapse/expand button - Enable collapsible on LiveNewsPanel and LiveWebcamsPanel - Add CSS to shrink panel to header height when collapsed - Clean up debug console.logs from undo handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(panels): add aria-expanded, persist collapsed state, and add missing i18n keys - Extract _applyCollapsed() helper to centralize DOM/state updates - Set aria-expanded on collapse button and toggle it on click (accessibility) - Persist collapsed state per panel in localStorage (worldmonitor-panel-collapsed) and restore it on page load after this.content is available - Add collapsePanel and expandPanel keys to en.json (fallback strings were always used since the keys were undefined) * fix(panels): update aria-label on toggle and fix isContentEditable null guard - _applyCollapsed now also updates aria-label alongside aria-expanded so screen readers announce the correct action after toggling - Add optional chaining to isContentEditable in the undo keyboard handler to match the existing ?.tagName guard on the same cast (Greptile P2) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Elie Habib <elie.habib@gmail.com>
1 parent 380b495 commit 34aa310

6 files changed

Lines changed: 102 additions & 7 deletions

File tree

src/app/event-handlers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ export class EventHandlerManager implements AppModule {
152152

153153
// Ensure restored panel fetches fresh data (otherwise it may show no content)
154154
const panel = this.ctx.panels[panelId];
155-
if (panel && 'fetchData' in panel) {
156-
(panel as any).fetchData();
155+
if (panel && 'fetchData' in panel && typeof (panel as { fetchData: unknown }).fetchData === 'function') {
156+
(panel as { fetchData: () => void }).fetchData();
157157
}
158158
}
159159

@@ -430,8 +430,8 @@ export class EventHandlerManager implements AppModule {
430430
// undo via Ctrl/Cmd+Z
431431
this.boundUndoHandler = (e: KeyboardEvent) => {
432432
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
433-
const tag = (e.target as HTMLElement).tagName;
434-
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return;
433+
const tag = (e.target as HTMLElement)?.tagName ?? '';
434+
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
435435
e.preventDefault();
436436
this.performUndo();
437437
}

src/components/LiveNewsPanel.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,7 @@ export class LiveNewsPanel extends Panel {
409409
private idleCallbackId: number | ReturnType<typeof setTimeout> | null = null;
410410

411411
constructor() {
412-
// allow users to close the live news panel
413-
super({ id: 'live-news', title: t('panels.liveNews'), className: 'panel-wide', closable: true });
412+
super({ id: 'live-news', title: t('panels.liveNews'), className: 'panel-wide', closable: true, collapsible: true });
414413
this.insertLiveCountBadge(OPTIONAL_LIVE_CHANNELS.length);
415414
this.youtubeOrigin = LiveNewsPanel.resolveYouTubeOrigin();
416415
this.playerElementId = `live-news-player-${Date.now()}`;

src/components/LiveWebcamsPanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class LiveWebcamsPanel extends Panel {
124124
private boundEmbedMessageHandler: (e: MessageEvent) => void;
125125

126126
constructor() {
127-
super({ id: 'live-webcams', title: t('panels.liveWebcams'), className: 'panel-wide', closable: true });
127+
super({ id: 'live-webcams', title: t('panels.liveWebcams'), className: 'panel-wide', closable: true, collapsible: true });
128128
this.insertLiveCountBadge(WEBCAM_FEEDS.length);
129129

130130
const prefs = loadWebcamPrefs(this.forceSingleView);

src/components/Panel.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface PanelOptions {
1616
infoTooltip?: string;
1717
premium?: 'locked' | 'enhanced';
1818
closable?: boolean;
19+
collapsible?: boolean;
1920
defaultRowSpan?: number;
2021
}
2122

@@ -60,6 +61,31 @@ function savePanelColSpan(panelId: string, span: number): void {
6061
localStorage.setItem(PANEL_COL_SPANS_KEY, JSON.stringify(spans));
6162
}
6263

64+
const PANEL_COLLAPSED_KEY = 'worldmonitor-panel-collapsed';
65+
66+
function loadPanelCollapsed(): Record<string, boolean> {
67+
try {
68+
const stored = localStorage.getItem(PANEL_COLLAPSED_KEY);
69+
return stored ? JSON.parse(stored) : {};
70+
} catch {
71+
return {};
72+
}
73+
}
74+
75+
function savePanelCollapsed(panelId: string, collapsed: boolean): void {
76+
const map = loadPanelCollapsed();
77+
if (collapsed) {
78+
map[panelId] = true;
79+
} else {
80+
delete map[panelId];
81+
}
82+
if (Object.keys(map).length === 0) {
83+
localStorage.removeItem(PANEL_COLLAPSED_KEY);
84+
} else {
85+
localStorage.setItem(PANEL_COLLAPSED_KEY, JSON.stringify(map));
86+
}
87+
}
88+
6389
function clearPanelColSpan(panelId: string): void {
6490
const spans = loadPanelColSpans();
6591
if (!(panelId in spans)) return;
@@ -208,6 +234,8 @@ export class Panel {
208234
private retryAttempt = 0;
209235
private _fetching = false;
210236
private _locked = false;
237+
private _collapsed = false;
238+
private _collapseBtn: HTMLButtonElement | null = null;
211239

212240
constructor(options: PanelOptions) {
213241
this.panelId = options.id;
@@ -274,6 +302,10 @@ export class Panel {
274302
this.header.appendChild(this.countEl);
275303
}
276304

305+
if (options.collapsible) {
306+
this.appendCollapseButton();
307+
}
308+
277309
if (options.closable !== false) {
278310
this.appendCloseButton();
279311
}
@@ -285,6 +317,10 @@ export class Panel {
285317
this.element.appendChild(this.header);
286318
this.element.appendChild(this.content);
287319

320+
if (this._collapseBtn && loadPanelCollapsed()[this.panelId]) {
321+
this._applyCollapsed(this._collapseBtn, true);
322+
}
323+
288324
this.content.addEventListener('click', (e) => {
289325
const target = (e.target as HTMLElement).closest('[data-panel-retry]');
290326
if (!target || this._fetching) return;
@@ -658,6 +694,35 @@ export class Panel {
658694
headerLeft.appendChild(badge);
659695
}
660696

697+
private _applyCollapsed(btn: HTMLButtonElement, collapsed: boolean): void {
698+
this._collapsed = collapsed;
699+
this.content.style.display = collapsed ? 'none' : '';
700+
this.element.classList.toggle('panel-collapsed', collapsed);
701+
btn.textContent = collapsed ? '▸' : '▾';
702+
const label = collapsed
703+
? (t('components.panel.expandPanel') ?? 'Expand')
704+
: (t('components.panel.collapsePanel') ?? 'Collapse');
705+
btn.setAttribute('aria-expanded', String(!collapsed));
706+
btn.setAttribute('aria-label', label);
707+
btn.title = label;
708+
}
709+
710+
protected appendCollapseButton(): void {
711+
const btn = h('button', {
712+
className: 'icon-btn panel-collapse-btn',
713+
'aria-label': t('components.panel.collapsePanel') ?? 'Collapse',
714+
'aria-expanded': 'true',
715+
title: t('components.panel.collapsePanel') ?? 'Collapse',
716+
}, '▾') as HTMLButtonElement;
717+
btn.addEventListener('click', (e) => {
718+
e.stopPropagation();
719+
this._applyCollapsed(btn, !this._collapsed);
720+
savePanelCollapsed(this.panelId, this._collapsed);
721+
});
722+
this._collapseBtn = btn;
723+
this.header.appendChild(btn);
724+
}
725+
661726
protected appendCloseButton(): void {
662727
const closeBtn = h('button', {
663728
className: 'icon-btn panel-close-btn',

src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,8 @@
19301930
"dragToResize": "Drag to resize (double-click to reset)",
19311931
"openSettings": "Open Settings",
19321932
"closePanel": "Close panel",
1933+
"collapsePanel": "Collapse panel",
1934+
"expandPanel": "Expand panel",
19331935
"addPanel": "Add Panel"
19341936
},
19351937
"languageSelector": {

src/styles/main.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,6 +1544,35 @@ body.panel-resize-active iframe {
15441544
}
15451545
}
15461546

1547+
/* ---- Collapse (▾) button on panels ---- */
1548+
.panel-header .panel-collapse-btn {
1549+
font-size: 12px;
1550+
opacity: 0;
1551+
transition: opacity 0.15s ease, background 0.15s ease;
1552+
order: 998;
1553+
}
1554+
1555+
.panel:hover .panel-collapse-btn,
1556+
.panel-collapse-btn:focus-visible {
1557+
opacity: 1;
1558+
}
1559+
1560+
.panel.panel-collapsed {
1561+
align-self: start;
1562+
height: auto !important;
1563+
min-height: 0 !important;
1564+
}
1565+
1566+
.panel-collapsed .panel-collapse-btn {
1567+
opacity: 0.7;
1568+
}
1569+
1570+
@media (hover: none) {
1571+
.panel-header .panel-collapse-btn {
1572+
opacity: 0.7;
1573+
}
1574+
}
1575+
15471576
/* ---- Add Panel (+) block ---- */
15481577
.add-panel-block {
15491578
display: flex;

0 commit comments

Comments
 (0)