Create visually rich, robust TypeScript UI components with panel architecture, combining enhanced features like retry logic, state persistence, and dynamic styling with a systematic workflow for visual enhancements.
Build visually rich, robust TypeScript UI components using a panel architecture pattern. This skill combines enhanced panel features (retry logic, state persistence) with a systematic workflow for adding visual enhancements (badges, thumbnails, dynamic styling).
Panel (base class)
├── element: HTMLElement (outer container, .panel)
│ ├── header: HTMLElement (.panel-header)
│ │ ├── headerLeft (.panel-header-left)
│ │ │ ├── title (.panel-title)
│ │ │ └── newBadge (.panel-new-badge) [optional]
│ │ ├── statusBadge (.panel-data-badge) [optional]
│ │ └── countEl (.panel-count) [optional]
│ ├── content: HTMLElement (.panel-content)
│ └── resizeHandle (.panel-resize-handle)
// Panel configuration
const PANEL_DEFAULTS = {
maxRetries: 3,
initialRetryDelay: 1000, // 1s
};
// Visual configuration
const SOURCE_COLORS: { [key: string]: string } = {
'Category A': '#FF6B6B',
'Category B': '#4ECDC4',
'default': '#95A5A6'
};
const THUMBNAIL_SIZE = 48; // pixels
export class Panel {
protected element: HTMLElement;
protected content: HTMLElement;
protected panelId: string;
private retryAttempts = 0;
private retryDelay = PANEL_DEFAULTS.initialRetryDelay;
constructor(options: PanelOptions) {
// ... existing constructor code ...
this.loadState();
}
protected async fetchWithRetry(url: string):<any> {
while (this.retryAttempts < PANEL_DEFAULTS.maxRetries) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
this.retryAttempts++;
if (this.retryAttempts >= PANEL_DEFAULTS.maxRetries) {
throw new Error(`Failed after ${PANEL_DEFAULTS.maxRetries} attempts: ${error.message}`);
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
this.retryDelay *= 2;
}
}
}
public saveState(): void {
localStorage.setItem(`panelState_${this.panelId}`, JSON.stringify({
isExpanded: !this.element.classList.contains('collapsed'),
width: this.element.style.width,
height: this.element.style.height
}));
}
public loadState(): void {
const savedState = localStorage.getItem(`panelState_${this.panelId}`);
if (savedState) {
const { isExpanded, width, height } = JSON.parse(savedState);
if (!isExpanded) this.element.classList.add('collapsed');
if (width) this.element.style.width = width;
if (height) this.element.style.height = height;
}
}
// Visual enhancement helpers
protected getColorForSource(source: string): string {
return SOURCE_COLORS[source] || SOURCE_COLORS['default'];
}
protected getThumbnailUrl(imageUrl?: string): string {
return imageUrl || `https://via.placeholder.com/${THUMBNAIL_SIZE}`;
}
}
export class NewsPanel extends Panel {
private refreshTimer: ReturnType<typeof setInterval> | null = null;
constructor() {
super({ id: 'news', title: 'News Feed', showCount: true });
this.fetchData();
this.refreshTimer = setInterval(() => this.fetchData(), 60_000);
}
private async fetchData(): Promise<void> {
try {
const newsItems = await this.fetchWithRetry('/api/news');
this.render(newsItems);
this.setCount(newsItems.length);
this.saveState();
} catch (err) {
this.showError(`Failed to load news: ${err.message}`, () => {
this.retryAttempts = 0;
this.retryDelay = PANEL_DEFAULTS.initialRetryDelay;
this.fetchData();
});
}
}
private render(items: NewsItem[]): void {
const html = items.map(item => `
<div class="news-item">
<img src="${this.getThumbnailUrl(item.imageUrl)}" class="news-thumbnail">
<div class="news-content">
<h3>${item.title}</h3>
<span class="news-badge" style="background-color: ${this.getColorForSource(item.source)};">
${item.source}
</span>
<p>${item.summary}</p>
</div>
</div>
`).join('');
this.setContent(`<div class="news-list">${html}</div>`);
}
public override destroy(): void {
if (this.refreshTimer) clearInterval(this.refreshTimer);
super.destroy();
}
}
/* Panel structure */
.panel {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
/* News item styles */
.news-item {
display: flex;
gap: 16px;
padding: 12px;
border-bottom: 1px solid #eee;
}
.news-thumbnail {
width: 48px;
height: 48px;
border-radius: 4px;
object-fit: cover;
}
.news-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
color: white;
margin-right: 8px;
}