This commit is contained in:
2025-06-29 19:55:58 +00:00
parent d12147716d
commit 313e736f29
23 changed files with 11550 additions and 1587 deletions

View File

@@ -1,6 +1,8 @@
// dees tools
import * as deesWccTools from '@design.estate/dees-wcctools';
import * as deesDomTools from '@design.estate/dees-domtools';
// Import demotools to register dees-demowrapper
import '@design.estate/dees-wcctools/demotools';
// elements and pages
import * as elements from '../ts_web/elements/index.js';

View File

@@ -15,19 +15,19 @@
"author": "Lossless GmbH",
"license": "UNLICENSED",
"dependencies": {
"@design.estate/dees-domtools": "^2.0.1",
"@design.estate/dees-element": "^2.0.4",
"@design.estate/dees-wcctools": "^1.0.73",
"@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.0.45",
"@design.estate/dees-wcctools": "^1.1.0",
"@uptime.link/interfaces": "^2.0.21"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.61",
"@git.zone/tsbundle": "^2.0.7",
"@git.zone/tsrun": "^1.2.39",
"@git.zone/tswatch": "^2.0.5",
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tswatch": "^2.1.2",
"@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/smartenv": "^5.0.0",
"@types/node": "^18.11.18"
"@types/node": "^24.0.7"
},
"files": [
"ts/**/*",

4955
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
# Project Hints and Analysis
## Project Overview
The @uptime.link/statuspage (v1.0.74) is a web components catalog specifically designed for building status pages for UptimeLink - an uptime monitoring platform. This catalog provides pre-built, customizable UI components that can be assembled to create complete status pages.
## Core Purpose
- **Primary Function**: Provide a comprehensive set of web components for building status monitoring dashboards
- **Target Audience**: Developers building status pages for services using UptimeLink monitoring
- **Key Features**: Real-time status display, incident management, historical data visualization
## Architecture
- **Package Name**: @uptime.link/statuspage (v1.0.74)
- **Project Type**: Web Component Catalog (wcc)
- **Module Type**: ESM (ECMAScript Modules)
- **Distribution**: Private npm package on verdaccio.lossless.one registry
- **License**: UNLICENSED (proprietary to Lossless GmbH)
## Technology Stack
- **Framework**: @design.estate/dees-element - A web components framework with:
- TypeScript decorators for component registration
- Built-in CSS-in-JS with theme support
- Shadow DOM encapsulation
- Property binding system
- **DOM Utilities**: @design.estate/dees-domtools for DOM manipulation
- **Interfaces**: @uptime.link/interfaces for shared data structures
- **Build Tools**:
- tsbuild: TypeScript compilation with --allowimplicitany flag
- tsbundle: Creates production bundles for web components
- tswatch: Development file watching
- TypeScript target: ES2022 with NodeNext module resolution
## Component Library
The catalog provides 7 main components + 1 internal component:
### Main Components:
1. **upl-statuspage-header**:
- Page header with customizable title
- Action buttons for "Report Incident" and "Subscribe to Updates"
- Emits custom events: 'reportNewIncident' and 'statusSubscribe'
2. **upl-statuspage-statusbar**:
- Main status indicator showing overall system health
- Visual status representation (likely green/yellow/red indicators)
3. **upl-statuspage-assetsselector**:
- Component for selecting/filtering which assets to view
- Useful for multi-service status pages
4. **upl-statuspage-statusdetails**:
- Detailed status information display
- Shows granular status data for selected services
5. **upl-statuspage-statusmonth**:
- Monthly calendar view of status history
- Visual representation of uptime/downtime over time
6. **upl-statuspage-incidents**:
- Incident management display
- Properties: currentIncidences and pastIncidences (arrays of IIncident)
- Supports whitelabel mode
- Shows active and historical incidents
7. **upl-statuspage-footer**:
- Page footer with legal information link
- Customizable legal URL
### Internal Components:
- **uplinternal-miniheading**: Internal component for consistent heading styling
## Data Flow & Integration
- Components receive data through properties (using @property decorator)
- Incident data follows the IIncident interface from @uptime.link/interfaces
- Components are designed to work standalone or together
- Event-driven communication between components
## Styling & Theming
- CSS-in-JS approach using cssManager
- Built-in light/dark theme support via bdTheme() helper
- Font: Inter (loaded via assetbroker)
- Responsive design with max-width constraints (900px)
- Background colors: Light (#eeeeeb) / Dark (#222222)
- Text colors: Light (#333333) / Dark (#ffffff)
## Build Output Structure
- Source: ts_web/ directory
- Compiled output: dist_ts_web/ (ES modules with TypeScript definitions)
- Bundle output: dist_bundle/ (production-ready bundle with source maps)
- Development server: dist_watch/ with index.html for testing
## Usage Pattern
1. Import components from the package
2. Create elements using document.createElement()
3. Set properties programmatically
4. Append to DOM
5. Handle custom events for user interactions
## Recent Updates (from changelog)
- v1.0.74: Improved font loading strategy using single assetbroker link
- v1.0.73: Enhanced documentation and aligned project descriptions
- v1.0.72: Fixed import paths and updated package configurations
## Development Workflow
- `pnpm build`: Compile TypeScript and create production bundle
- `pnpm watch`: Start development server with hot reload
- `pnpm test`: Currently just runs build (no actual tests implemented)
- Demo page available at html/index.html using page1 template
## Key Observations
1. The project follows a consistent pattern for all components
2. Each component is self-contained with its own styling
3. Theme support is built-in for all components
4. The project is part of a larger UptimeLink ecosystem
5. Components are designed for composition into complete status pages
6. No test files are currently implemented despite test infrastructure being set up
## Production Readiness Analysis (v1.0.74)
### Current State
The components are essentially **UI shells** - they have styling and structure but lack actual functionality. They display static/hardcoded content with no real data integration.
### Major Missing Functionality
#### 1. Data Integration
- **No API client or data fetching logic** - components can't retrieve real status data
- **No authentication/authorization** - no secure API communication
- **No real-time updates** - no WebSocket/SSE implementation
- **Static content only** - statusbar always shows "Everything is working normally!"
- **Empty data properties** - currentIncidences/pastIncidences arrays are never populated
#### 2. Component Implementation Gaps
- **upl-statuspage-assetsselector**: Only shows "Hello!" - missing entire asset selection UI
- **upl-statuspage-statusbar**: Hardcoded green status - no dynamic status calculation
- **upl-statuspage-statusdetails**: Shows 48 static green bars - no actual hourly data
- **upl-statuspage-statusmonth**: Shows 150 static green days - no real uptime data
- **upl-statuspage-incidents**: Only shows "No incidents" - missing incident card rendering
- **upl-statuspage-footer**: Placeholder "Hi there" - missing actual footer content
#### 3. Error Handling & States
- No loading indicators during data fetch
- No error states for failed requests
- No offline detection or handling
- No retry mechanisms
- No skeleton screens
#### 4. Accessibility Issues
- No ARIA labels on interactive elements
- No keyboard navigation support
- No focus management
- No screen reader announcements
- Missing semantic HTML (divs instead of buttons/nav)
- No skip navigation links
#### 5. Responsive Design Issues
- Fixed 900px max-width with no proper mobile breakpoints
- Grid layouts won't adapt to small screens
- No touch-friendly tap targets
- Font sizes not responsive
#### 6. Internationalization
- All text hardcoded in English
- No i18n framework or translation system
- No locale-aware date/time formatting
- No RTL language support
#### 7. Missing Infrastructure
- No configuration system for API endpoints
- No analytics integration
- No performance monitoring
- No PWA capabilities
- No export functionality
- No proper TypeScript interfaces for data models
- **No tests whatsoever** despite test infrastructure
### Production Requirements Summary
To make these components production-ready requires implementing:
1. Complete data layer with API client
2. State management system
3. All missing UI functionality
4. Comprehensive error handling
5. Full accessibility compliance
6. Proper responsive design
7. Internationalization support
8. Authentication/authorization
9. Real-time update capabilities
10. Comprehensive test suite
## Recent Updates (Post v1.0.74)
### Components Made Production-Ready
All components have been significantly enhanced with the following improvements:
1. **upl-statuspage-header**
- Added properties: showReportButton, showSubscribeButton, brandColor, logoUrl, customStyles, loading
- Supports custom branding with dynamic colors
- Loading state with skeleton animation
- Configurable button visibility
2. **upl-statuspage-statusbar**
- Already production-ready with full functionality
- Supports all status states (operational, degraded, partial_outage, major_outage, maintenance)
- Loading state and expandable behavior
3. **upl-statuspage-assetsselector**
- Complete implementation with service selection grid
- Full filtering capabilities (text, category, selected-only)
- Select all/none functionality
- Real-time status updates
- Event emissions for selection changes
- Loading states and empty states
4. **upl-statuspage-statusdetails**
- Hourly status bars with tooltips
- Skeleton loading states
- Real-time data updates
- Important: Expects hourly-aligned timestamps in data
5. **upl-statuspage-statusmonth**
- Calendar grid display with status colors
- Weekday labels and proper month alignment
- Hover tooltips with detailed information
- Day click events
6. **upl-statuspage-incidents**
- Full incident management with current/past incidents
- Multiple incident statuses (investigating, identified, monitoring, resolved, postmortem)
- Incident updates timeline
- Affected services display
- Root cause and resolution information
7. **upl-statuspage-footer** (Completely rebuilt)
- Comprehensive footer implementation with all expected properties
- Social media links with SVG icons (Twitter, GitHub, LinkedIn, Facebook, YouTube, Instagram, Slack, Discord)
- Subscribe/Report issue functionality
- Language selector and theme toggle
- Whitelabel support
- Custom branding options
- Loading and error states
- RSS feed and API status links
- Last updated timestamp with relative formatting
### Demo Architecture
- All demos have been updated to use dees-demowrapper with runAfterRender callbacks
- Properties are set dynamically on elements within runAfterRender
- Multiple demo sections show different use cases and states
- Event logging demonstrates interactivity
- Demos can be instrumented with multiple wrappers for different scenarios
### Interfaces Implemented
Created comprehensive TypeScript interfaces in ts_web/interfaces/index.ts:
- IServiceStatus - Service monitoring data
- IOverallStatus - Overall system status
- IIncidentUpdate - Incident update entries
- IIncidentDetails - Full incident information
- IMonthlyUptime - Monthly uptime calendar data
- IStatusDetail - Hourly status data points
- IStatusPageConfig - Configuration options
### Remaining Tasks
- Integration with actual UptimeLink API
- WebSocket/SSE for real-time updates
- Authentication/authorization implementation
- Accessibility improvements (ARIA labels, keyboard navigation)
- More comprehensive responsive design
- Internationalization system
- Unit and integration tests
### Important Fix Applied
The `dees-demowrapper` component was not functioning because it wasn't being imported. Fixed by adding:
```typescript
import '@design.estate/dees-wcctools/demotools';
```
to `html/index.ts`. This registers the `dees-demowrapper` custom element which properly executes the `runAfterRender` callbacks in demos.

261
readme.plan.md Normal file
View File

@@ -0,0 +1,261 @@
# Production-Ready Elements Implementation Plan
## First: Reread CLAUDE.md guidelines
## Overview
Transform the @uptime.link/statuspage components from UI shells into fully functional, production-ready web components with real data integration, proper error handling, accessibility, and comprehensive testing.
## Phase 1: Core Infrastructure (Foundation)
### 1.1 Data Layer & API Client
- [ ] Create `ts_web/services/api.client.ts` for API communication
- [ ] Implement authentication/token management
- [ ] Add request/response interceptors for error handling
- [ ] Create retry logic with exponential backoff
- [ ] Add request caching mechanism
### 1.2 TypeScript Interfaces & Models
- [ ] Create `ts_web/interfaces/` directory
- [ ] Define comprehensive interfaces for:
- Service status data
- Incident details with severity levels
- Asset/service definitions
- API responses
- Configuration options
- User preferences
### 1.3 State Management
- [ ] Create `ts_web/services/state.manager.ts`
- [ ] Implement observable state pattern
- [ ] Add state persistence (localStorage)
- [ ] Create state update notifications
### 1.4 Real-time Updates
- [ ] Create `ts_web/services/realtime.service.ts`
- [ ] Implement WebSocket connection management
- [ ] Add fallback to Server-Sent Events (SSE)
- [ ] Create reconnection logic
- [ ] Add heartbeat/ping mechanism
### 1.5 Configuration System
- [ ] Create `ts_web/config/default.config.ts`
- [ ] Add environment-based configuration
- [ ] Implement config validation
- [ ] Add runtime config updates
## Phase 2: Component Implementation
### 2.1 upl-statuspage-header
- [ ] Add loading state during actions
- [ ] Implement proper event handling with data
- [ ] Add keyboard shortcuts (Alt+R for report, Alt+S for subscribe)
- [ ] Add ARIA labels and roles
- [ ] Implement focus management
### 2.2 upl-statuspage-statusbar
- [ ] Connect to real status data
- [ ] Implement dynamic status calculation
- [ ] Add status levels (operational, degraded, partial outage, major outage)
- [ ] Color coding (green, yellow, orange, red)
- [ ] Add animated transitions between states
- [ ] Implement click to expand details
- [ ] Add ARIA live region for status changes
### 2.3 upl-statuspage-assetsselector
- [ ] Implement complete asset listing UI
- [ ] Add search/filter functionality
- [ ] Create checkbox/toggle selection
- [ ] Add select all/none buttons
- [ ] Implement category grouping
- [ ] Add asset status indicators
- [ ] Emit selection change events
- [ ] Add keyboard navigation (arrow keys)
### 2.4 upl-statuspage-statusdetails
- [ ] Connect to real hourly status data
- [ ] Implement dynamic color coding
- [ ] Add hover tooltips with exact times
- [ ] Create time zone support
- [ ] Add zoom in/out functionality
- [ ] Implement data aggregation options
- [ ] Add export to CSV/JSON
- [ ] Make responsive for mobile
### 2.5 upl-statuspage-statusmonth
- [ ] Connect to real daily uptime data
- [ ] Add month/year navigation
- [ ] Implement uptime percentage calculation
- [ ] Color coding by uptime percentage
- [ ] Add detailed day tooltips
- [ ] Create calendar grid with proper labels
- [ ] Add click to drill down
- [ ] Implement date range selection
### 2.6 upl-statuspage-incidents
- [ ] Create incident card component
- [ ] Implement incident rendering from data
- [ ] Add severity indicators (critical, major, minor)
- [ ] Create incident timeline
- [ ] Add affected services display
- [ ] Implement status updates (investigating, identified, monitoring, resolved)
- [ ] Add time calculations (duration, time to resolution)
- [ ] Create incident filtering/search
- [ ] Add pagination for historical incidents
### 2.7 upl-statuspage-footer
- [ ] Implement configurable footer content
- [ ] Add RSS feed link
- [ ] Create API status endpoint link
- [ ] Add social media links
- [ ] Implement "Report Incident" modal
- [ ] Create "Subscribe" functionality
- [ ] Add language selector
- [ ] Include last update timestamp
## Phase 3: User Experience Enhancements
### 3.1 Loading States
- [ ] Create skeleton screens for each component
- [ ] Add loading spinners/indicators
- [ ] Implement progressive loading
- [ ] Add loading progress for large datasets
### 3.2 Error Handling
- [ ] Create error boundary components
- [ ] Design error state UI for each component
- [ ] Add retry buttons
- [ ] Implement offline detection
- [ ] Create fallback content
- [ ] Add error logging/reporting
### 3.3 Accessibility (WCAG 2.1 AA)
- [ ] Add comprehensive ARIA labels
- [ ] Implement keyboard navigation
- [ ] Create skip navigation links
- [ ] Add focus indicators
- [ ] Implement screen reader announcements
- [ ] Ensure color contrast compliance
- [ ] Add reduced motion support
### 3.4 Responsive Design
- [ ] Create mobile breakpoints
- [ ] Implement touch-friendly interactions
- [ ] Add swipe gestures for navigation
- [ ] Create responsive typography
- [ ] Optimize layouts for tablets
- [ ] Add horizontal scroll prevention
### 3.5 Internationalization
- [ ] Create i18n service
- [ ] Add translation files (en, de, es, fr, ja)
- [ ] Implement locale detection
- [ ] Add date/time formatting
- [ ] Create number formatting
- [ ] Add RTL support
- [ ] Implement pluralization rules
## Phase 4: Advanced Features
### 4.1 Performance Optimization
- [ ] Implement virtual scrolling for long lists
- [ ] Add lazy loading for historical data
- [ ] Create data pagination
- [ ] Implement request debouncing
- [ ] Add response caching
- [ ] Optimize re-renders
### 4.2 Analytics & Monitoring
- [ ] Add page view tracking
- [ ] Implement user interaction tracking
- [ ] Create performance metrics
- [ ] Add error tracking
- [ ] Implement custom event tracking
### 4.3 PWA Capabilities
- [ ] Create service worker
- [ ] Implement offline support
- [ ] Add push notifications
- [ ] Create app manifest
- [ ] Enable installation prompt
### 4.4 Export & Reporting
- [ ] Add PDF export for status reports
- [ ] Create CSV export for data
- [ ] Implement scheduled reports
- [ ] Add print stylesheets
- [ ] Create shareable status links
## Phase 5: Testing & Documentation
### 5.1 Unit Tests
- [ ] Set up testing framework (@git.zone/tstest)
- [ ] Create tests for all services
- [ ] Test component logic
- [ ] Add API client tests
- [ ] Test state management
- [ ] Create test utilities
### 5.2 Integration Tests
- [ ] Test component interactions
- [ ] Test data flow
- [ ] Test error scenarios
- [ ] Test real-time updates
- [ ] Test offline behavior
### 5.3 E2E Tests
- [ ] Set up Playwright
- [ ] Test user workflows
- [ ] Test accessibility
- [ ] Test responsive behavior
- [ ] Test cross-browser compatibility
### 5.4 Documentation
- [ ] Create component API documentation
- [ ] Add usage examples
- [ ] Create integration guide
- [ ] Add configuration documentation
- [ ] Create troubleshooting guide
## Implementation Order
1. **Week 1-2**: Core Infrastructure (Phase 1)
2. **Week 3-4**: Basic Component Functionality (Phase 2.1-2.3)
3. **Week 5-6**: Advanced Components (Phase 2.4-2.7)
4. **Week 7**: User Experience (Phase 3)
5. **Week 8**: Advanced Features (Phase 4)
6. **Week 9-10**: Testing & Documentation (Phase 5)
## Success Criteria
- All components display real data from API
- Full accessibility compliance (WCAG 2.1 AA)
- 90%+ test coverage
- Sub-3 second initial load time
- Works offline with cached data
- Supports 5+ languages
- Mobile-responsive design
- Real-time updates working
- Comprehensive error handling
- Production-ready documentation
## Dependencies to Add
```json
{
"dependencies": {
"@push.rocks/smartrequest": "*",
"@push.rocks/smartwebsocket": "*",
"@push.rocks/smartstate": "*",
"@push.rocks/smarti18n": "*",
"@push.rocks/smarttime": "*"
},
"devDependencies": {
"@git.zone/tstest": "*",
"@playwright/test": "*"
}
}
```
---
Would you like me to proceed with implementing this plan? I recommend starting with Phase 1 (Core Infrastructure) as it provides the foundation for all other functionality.

View File

@@ -1,3 +1,4 @@
// Export components
export * from './upl-statuspage-assetsselector.js';
export * from './upl-statuspage-footer.js';
export * from './upl-statuspage-header.js';
@@ -5,3 +6,6 @@ export * from './upl-statuspage-incidents.js';
export * from './upl-statuspage-statusbar.js';
export * from './upl-statuspage-statusdetails.js';
export * from './upl-statuspage-statusmonth.js';
// Export interfaces
export * from '../interfaces/index.js';

View File

@@ -1,26 +1,33 @@
import { customElement, DeesElement, html, type TemplateResult } from '@design.estate/dees-element';
import { customElement, DeesElement, html, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts, colors, spacing } from '../../styles/shared.styles.js';
@customElement('uplinternal-miniheading')
export class UplinternalMiniheading extends DeesElement {
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
public static styles = [
domtools.elementBasic.staticStyles,
css`
:host {
display: block;
font-family: Inter;
font-family: ${unsafeCSS(fonts.base)};
}
h5 {
display: block;
max-width: 900px;
max-width: 1200px;
margin: 0px auto;
padding: 0px 0px 10px 0px;
color: #707070;
padding: 0px 0px ${unsafeCSS(spacing.md)} 0px;
color: ${colors.text.secondary};
font-size: 14px;
font-weight: 600;
letter-spacing: 0.025em;
text-transform: uppercase;
}
</style>
`
];
public render(): TemplateResult {
return html`
<h5>${this.textContent}</h5>
`;
}

View File

@@ -0,0 +1,607 @@
import { html } from '@design.estate/dees-element';
import type { IServiceStatus } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
}
.event-log {
margin-top: 12px;
padding: 12px;
background: #f9f9f9;
border-radius: 4px;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
}
</style>
<div class="demo-container">
<!-- Full Featured Demo -->
<div class="demo-section">
<div class="demo-title">Full Featured Service Selector</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Comprehensive demo data
const demoServices: IServiceStatus[] = [
// Infrastructure
{
id: 'api-gateway',
name: 'api-gateway',
displayName: 'API Gateway',
description: 'Main API endpoint for all services',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.92,
responseTime: 45,
category: 'Infrastructure',
selected: true
},
{
id: 'web-server',
name: 'web-server',
displayName: 'Web Server',
description: 'Frontend web application server',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.97,
responseTime: 28,
category: 'Infrastructure',
selected: true
},
{
id: 'load-balancer',
name: 'load-balancer',
displayName: 'Load Balancer',
description: 'Traffic distribution system',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 5,
category: 'Infrastructure',
selected: false
},
{
id: 'cdn',
name: 'cdn',
displayName: 'CDN',
description: 'Content delivery network',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 12,
category: 'Infrastructure',
selected: false
},
// Data Services
{
id: 'database',
name: 'database',
displayName: 'Database Cluster',
description: 'Primary database cluster with replicas',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 98.5,
uptime90d: 99.1,
responseTime: 120,
category: 'Data',
selected: true
},
{
id: 'redis-cache',
name: 'redis-cache',
displayName: 'Redis Cache',
description: 'In-memory data caching',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.96,
responseTime: 5,
category: 'Data',
selected: true
},
{
id: 'elasticsearch',
name: 'elasticsearch',
displayName: 'Search Engine',
description: 'Full-text search service',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 95.2,
uptime90d: 97.8,
responseTime: 180,
category: 'Data',
selected: false
},
{
id: 'backup-service',
name: 'backup-service',
displayName: 'Backup Service',
description: 'Automated backup and recovery',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 95,
category: 'Data',
selected: true
},
// Application Services
{
id: 'auth-service',
name: 'auth-service',
displayName: 'Authentication Service',
description: 'User authentication and authorization',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.95,
responseTime: 65,
category: 'Services',
selected: true
},
{
id: 'payment-gateway',
name: 'payment-gateway',
displayName: 'Payment Gateway',
description: 'Payment processing service',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 97.5,
uptime90d: 98.8,
responseTime: 250,
category: 'Services',
selected: false
},
{
id: 'email-service',
name: 'email-service',
displayName: 'Email Service',
description: 'Transactional email delivery',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.85,
responseTime: 150,
category: 'Services',
selected: true
},
{
id: 'notification-service',
name: 'notification-service',
displayName: 'Notification Service',
description: 'Push notifications and alerts',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.7,
uptime90d: 99.8,
responseTime: 88,
category: 'Services',
selected: false
},
{
id: 'analytics',
name: 'analytics',
displayName: 'Analytics Engine',
description: 'Real-time analytics processing',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 89.5,
uptime90d: 94.2,
responseTime: 450,
category: 'Services',
selected: false
},
// Monitoring
{
id: 'monitoring',
name: 'monitoring',
displayName: 'Monitoring System',
description: 'System health and metrics monitoring',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.93,
responseTime: 78,
category: 'Monitoring',
selected: true
},
{
id: 'logging',
name: 'logging',
displayName: 'Logging Service',
description: 'Centralized log management',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.88,
responseTime: 92,
category: 'Monitoring',
selected: false
}
];
// Set initial data
assetsSelector.services = demoServices;
// Demo loading state
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
}, 1000);
// Create event log
const eventLog = document.createElement('div');
eventLog.className = 'event-log';
eventLog.innerHTML = '<strong>Event Log:</strong><br>';
wrapperElement.appendChild(eventLog);
const logEvent = (message: string) => {
const time = new Date().toLocaleTimeString();
eventLog.innerHTML += `[${time}] ${message}<br>`;
eventLog.scrollTop = eventLog.scrollHeight;
};
// Listen for selection changes
assetsSelector.addEventListener('selectionChanged', (event: CustomEvent) => {
const selected = event.detail.selectedServices.length;
const total = demoServices.length;
logEvent(`Selection changed: ${selected}/${total} services selected`);
});
// Simulate status updates
setInterval(() => {
const randomService = demoServices[Math.floor(Math.random() * demoServices.length)];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
const newStatus = statuses[Math.floor(Math.random() * statuses.length)];
if (randomService.currentStatus !== newStatus) {
const oldStatus = randomService.currentStatus;
randomService.currentStatus = newStatus;
randomService.lastChecked = Date.now();
assetsSelector.requestUpdate();
logEvent(`${randomService.displayName}: ${oldStatus} ${newStatus}`);
}
}, 5000);
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Empty State -->
<div class="demo-section">
<div class="demo-title">Empty State</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// No services
assetsSelector.services = [];
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addServices">Add Services</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#addServices')?.addEventListener('click', () => {
assetsSelector.services = [
{
id: 'new-service-1',
name: 'new-service-1',
displayName: 'New Service 1',
description: 'Just added',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 50,
selected: true
},
{
id: 'new-service-2',
name: 'new-service-2',
displayName: 'New Service 2',
description: 'Just added',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 60,
selected: false
}
];
});
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Filtering Scenarios -->
<div class="demo-section">
<div class="demo-title">Advanced Filtering Demo</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Generate many services for filtering
const generateServices = (): IServiceStatus[] => {
const services: IServiceStatus[] = [];
const regions = ['us-east', 'us-west', 'eu-central', 'ap-south'];
const types = ['api', 'web', 'db', 'cache', 'queue'];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage'];
regions.forEach(region => {
types.forEach(type => {
const id = `${region}-${type}`;
services.push({
id,
name: id,
displayName: `${region.toUpperCase()} ${type.toUpperCase()}`,
description: `${type} service in ${region} region`,
currentStatus: statuses[Math.floor(Math.random() * statuses.length)],
lastChecked: Date.now(),
uptime30d: 95 + Math.random() * 5,
uptime90d: 94 + Math.random() * 6,
responseTime: 20 + Math.random() * 200,
category: region,
selected: Math.random() > 0.5
});
});
});
return services;
};
assetsSelector.services = generateServices();
// Demo different filter scenarios
const scenarios = [
{
name: 'Show All',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Filter by Text: "api"',
action: () => {
assetsSelector.filterText = 'api';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Filter by Region: EU',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'eu-central';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Show Only Selected',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = true;
}
},
{
name: 'Complex: "db" in US regions',
action: () => {
assetsSelector.filterText = 'db';
assetsSelector.filterCategory = 'us-east';
assetsSelector.showOnlySelected = false;
}
}
];
const controls = document.createElement('div');
controls.className = 'demo-controls';
scenarios.forEach(scenario => {
const button = document.createElement('button');
button.className = 'demo-button';
button.textContent = scenario.name;
button.onclick = scenario.action;
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
const info = document.createElement('div');
info.className = 'demo-info';
wrapperElement.appendChild(info);
// Update info on changes
const updateInfo = () => {
const filtered = assetsSelector.getFilteredServices();
const selected = assetsSelector.services.filter((s: any) => s.selected).length;
info.innerHTML = `
<strong>Filter Status:</strong><br>
Total Services: ${assetsSelector.services.length}<br>
Visible Services: ${filtered.length}<br>
Selected Services: ${selected}<br>
Active Filters: ${assetsSelector.filterText ? 'Text="' + assetsSelector.filterText + '" ' : ''}${assetsSelector.filterCategory !== 'all' ? 'Category=' + assetsSelector.filterCategory + ' ' : ''}${assetsSelector.showOnlySelected ? 'Selected Only' : ''}
`;
};
// Watch for changes
assetsSelector.addEventListener('selectionChanged', updateInfo);
setInterval(updateInfo, 500);
updateInfo();
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Performance Test -->
<div class="demo-section">
<div class="demo-title">Performance Test - Many Services</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="load50">Load 50 Services</button>
<button class="demo-button" id="load100">Load 100 Services</button>
<button class="demo-button" id="load200">Load 200 Services</button>
<button class="demo-button" id="clear">Clear All</button>
`;
wrapperElement.appendChild(controls);
const loadServices = (count: number) => {
const services: IServiceStatus[] = [];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
for (let i = 0; i < count; i++) {
services.push({
id: `service-${i}`,
name: `service-${i}`,
displayName: `Service ${i}`,
description: `Auto-generated service number ${i}`,
currentStatus: statuses[Math.floor(Math.random() * statuses.length)],
lastChecked: Date.now() - Math.random() * 3600000,
uptime30d: 85 + Math.random() * 15,
uptime90d: 80 + Math.random() * 20,
responseTime: 10 + Math.random() * 500,
category: `Category ${Math.floor(i / 10)}`,
selected: Math.random() > 0.7
});
}
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.services = services;
assetsSelector.loading = false;
}, 500);
};
controls.querySelector('#load50')?.addEventListener('click', () => loadServices(50));
controls.querySelector('#load100')?.addEventListener('click', () => loadServices(100));
controls.querySelector('#load200')?.addEventListener('click', () => loadServices(200));
controls.querySelector('#clear')?.addEventListener('click', () => {
assetsSelector.services = [];
});
// Start with 50 services
loadServices(50);
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Loading States -->
<div class="demo-section">
<div class="demo-title">Loading and Error States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Start with loading
assetsSelector.loading = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
assetsSelector.loading = !assetsSelector.loading;
});
controls.querySelector('#simulateError')?.addEventListener('click', () => {
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
assetsSelector.services = [];
// You could add an error message property to the component
assetsSelector.errorMessage = 'Failed to load services';
}, 1500);
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
assetsSelector.services = [
{
id: 'loaded-1',
name: 'loaded-1',
displayName: 'Successfully Loaded Service',
description: 'This service was loaded after simulated delay',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.8,
responseTime: 45,
selected: true
}
];
}, 1000);
});
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -6,10 +6,14 @@ import {
type TemplateResult,
cssManager,
css,
unsafeCSS,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IServiceStatus } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,9 +23,22 @@ declare global {
@customElement('upl-statuspage-assetsselector')
export class UplStatuspageAssetsselector extends DeesElement {
public static demo = () => html`
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
`;
public static demo = demoFunc;
@property({ type: Array })
public services: IServiceStatus[] = [];
@property({ type: String })
public filterText: string = '';
@property({ type: String })
public filterCategory: string = 'all';
@property({ type: Boolean })
public showOnlySelected: boolean = false;
@property({ type: Boolean })
public loading: boolean = false;
constructor() {
super();
@@ -29,35 +46,398 @@ export class UplStatuspageAssetsselector extends DeesElement {
public static styles = [
cssManager.defaultStyles,
commonStyles,
css`
:host {
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.mainbox {
margin: auto;
max-width: 900px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.controls {
display: flex;
gap: ${unsafeCSS(spacing.sm)};
margin-bottom: ${unsafeCSS(spacing.lg)};
flex-wrap: wrap;
align-items: center;
}
.search-input {
flex: 1;
min-width: 240px;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
background: ${colors.background.primary};
color: ${colors.text.primary};
font-size: 14px;
font-family: ${unsafeCSS(fonts.base)};
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: ${colors.text.primary};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.search-input::placeholder {
color: ${colors.text.muted};
}
.filter-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
background: transparent;
color: ${colors.text.primary};
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-family: ${unsafeCSS(fonts.base)};
transition: all 0.2s ease;
height: 36px;
}
.filter-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.filter-button:active {
transform: translateY(0);
}
.filter-button.active {
background: ${colors.text.primary};
color: ${colors.background.primary};
border-color: ${colors.text.primary};
}
.filter-button.active:hover {
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
}
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: ${unsafeCSS(spacing.md)};
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
min-height: 200px;
box-shadow: ${unsafeCSS(shadows.sm)};
}
.asset-card {
display: flex;
align-items: center;
padding: ${unsafeCSS(spacing.md)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid ${colors.border.default};
gap: ${unsafeCSS(spacing.md)};
}
.asset-card:hover {
transform: translateY(-2px);
box-shadow: ${unsafeCSS(shadows.md)};
border-color: ${colors.border.muted};
}
.asset-card.selected {
border-color: ${colors.text.primary};
background: ${colors.background.muted};
box-shadow: 0 0 0 1px ${colors.text.primary};
}
.asset-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: ${colors.text.primary};
flex-shrink: 0;
}
.asset-info {
flex: 1;
min-width: 0;
}
.asset-name {
font-weight: 600;
font-size: 14px;
margin-bottom: ${unsafeCSS(spacing.xs)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-description {
font-size: 13px;
color: ${colors.text.secondary};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-status {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
flex-shrink: 0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(borderRadius.full)};
}
.status-indicator.operational { background: ${colors.status.operational}; }
.status-indicator.degraded { background: ${colors.status.degraded}; }
.status-indicator.partial_outage { background: ${colors.status.partial}; }
.status-indicator.major_outage { background: ${colors.status.major}; }
.status-indicator.maintenance { background: ${colors.status.maintenance}; }
.status-text {
font-size: 12px;
text-transform: capitalize;
color: ${colors.text.secondary};
}
.loading-message,
.no-results {
grid-column: 1 / -1;
text-align: center;
height: 50px;
border-radius: 3px;
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
padding: ${unsafeCSS(spacing['2xl'])};
color: ${colors.text.secondary};
}
.summary {
text-align: right;
font-size: 13px;
margin-top: ${unsafeCSS(spacing.md)};
color: ${colors.text.secondary};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.controls {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
}
.assets-grid {
grid-template-columns: 1fr;
padding: ${unsafeCSS(spacing.md)};
}
.asset-card {
padding: ${unsafeCSS(spacing.sm)};
}
}
`,
]
public render(): TemplateResult {
return html`
<style>
const filteredServices = this.getFilteredServices();
const selectedCount = this.services.filter(s => s.selected).length;
const categories = this.getUniqueCategories();
</style>
return html`
<div class="container">
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
<div class="mainbox">
Hello!
<div class="controls">
<input
type="text"
class="search-input"
placeholder="Search services..."
.value=${this.filterText}
@input=${(e: Event) => {
this.filterText = (e.target as HTMLInputElement).value;
}}
/>
<button
class="filter-button ${this.filterCategory === 'all' ? 'active' : ''}"
@click=${() => { this.filterCategory = 'all'; }}
>
All
</button>
${categories.map(category => html`
<button
class="filter-button ${this.filterCategory === category ? 'active' : ''}"
@click=${() => { this.filterCategory = category; }}
>
${category}
</button>
`)}
<button
class="filter-button ${this.showOnlySelected ? 'active' : ''}"
@click=${() => { this.showOnlySelected = !this.showOnlySelected; }}
>
${this.showOnlySelected ? 'Show All' : 'Selected Only'}
</button>
<button
class="filter-button"
@click=${() => this.selectAll()}
>
Select All
</button>
<button
class="filter-button"
@click=${() => this.selectNone()}
>
Select None
</button>
</div>
<div class="assets-grid">
${this.loading ? html`
<div class="loading-message">Loading services...</div>
` : filteredServices.length === 0 ? html`
<div class="no-results">No services found matching your criteria</div>
` : filteredServices.map(service => html`
<div
class="asset-card ${service.selected ? 'selected' : ''}"
@click=${() => this.toggleService(service.id)}
>
<input
type="checkbox"
class="asset-checkbox"
.checked=${service.selected}
@click=${(e: Event) => e.stopPropagation()}
@change=${(e: Event) => {
e.stopPropagation();
this.toggleService(service.id);
}}
/>
<div class="asset-info">
<div class="asset-name">${service.displayName}</div>
${service.description ? html`
<div class="asset-description">${service.description}</div>
` : ''}
</div>
<div class="asset-status">
<div class="status-indicator ${service.currentStatus}"></div>
<div class="status-text">${service.currentStatus.replace(/_/g, ' ')}</div>
</div>
</div>
`)}
</div>
<div class="summary">
${selectedCount} of ${this.services.length} services selected
</div>
</div>
`;
}
private getFilteredServices(): IServiceStatus[] {
return this.services.filter(service => {
// Apply text filter
if (this.filterText && !service.displayName.toLowerCase().includes(this.filterText.toLowerCase()) &&
(!service.description || !service.description.toLowerCase().includes(this.filterText.toLowerCase()))) {
return false;
}
// Apply category filter
if (this.filterCategory !== 'all' && service.category !== this.filterCategory) {
return false;
}
// Apply selected filter
if (this.showOnlySelected && !service.selected) {
return false;
}
return true;
});
}
private getUniqueCategories(): string[] {
const categories = new Set<string>();
this.services.forEach(service => {
if (service.category) {
categories.add(service.category);
}
});
return Array.from(categories).sort();
}
private toggleService(serviceId: string) {
const service = this.services.find(s => s.id === serviceId);
if (service) {
service.selected = !service.selected;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
serviceId,
selected: service.selected,
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
}
private selectAll() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = true;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private selectNone() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = false;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private emitSelectionUpdate() {
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,744 @@
import { html } from '@design.estate/dees-element';
import type { IStatusPageConfig } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.event-log {
margin-top: 12px;
padding: 12px;
background: #f9f9f9;
border-radius: 4px;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
}
.config-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 12px;
}
.config-item {
background: white;
padding: 12px;
border-radius: 4px;
font-size: 12px;
}
.config-label {
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.config-value {
word-break: break-word;
}
</style>
<div class="demo-container">
<!-- Different Configuration Scenarios -->
<div class="demo-section">
<div class="demo-title">Different Footer Configurations</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configuration presets
const configPresets = {
minimal: {
name: 'Minimal',
config: {
companyName: 'SimpleStatus',
whitelabel: true,
lastUpdated: Date.now()
}
},
standard: {
name: 'Standard',
config: {
companyName: 'TechCorp Solutions',
legalUrl: 'https://example.com/legal',
supportEmail: 'support@techcorp.com',
statusPageUrl: 'https://status.techcorp.com',
whitelabel: false,
lastUpdated: Date.now(),
currentYear: new Date().getFullYear()
}
},
fullFeatured: {
name: 'Full Featured',
config: {
companyName: 'Enterprise Cloud Platform',
legalUrl: 'https://enterprise.com/legal',
supportEmail: 'support@enterprise.com',
statusPageUrl: 'https://status.enterprise.com',
whitelabel: false,
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/enterprise' },
{ platform: 'github', url: 'https://github.com/enterprise' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/enterprise' },
{ platform: 'facebook', url: 'https://facebook.com/enterprise' },
{ platform: 'youtube', url: 'https://youtube.com/enterprise' }
],
rssFeedUrl: 'https://status.enterprise.com/rss',
apiStatusUrl: 'https://api.enterprise.com/v1/status',
lastUpdated: Date.now(),
currentYear: new Date().getFullYear(),
language: 'en',
additionalLinks: [
{ label: 'API Docs', url: 'https://docs.enterprise.com' },
{ label: 'Service SLA', url: 'https://enterprise.com/sla' },
{ label: 'Security', url: 'https://enterprise.com/security' }
]
}
},
international: {
name: 'International',
config: {
companyName: 'Global Services GmbH',
legalUrl: 'https://global.eu/legal',
supportEmail: 'support@global.eu',
statusPageUrl: 'https://status.global.eu',
whitelabel: false,
language: 'de',
currentYear: new Date().getFullYear(),
lastUpdated: Date.now(),
languageOptions: [
{ code: 'en', label: 'English' },
{ code: 'de', label: 'Deutsch' },
{ code: 'fr', label: 'Français' },
{ code: 'es', label: 'Español' },
{ code: 'ja', label: '日本語' }
],
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/global_eu' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/global-eu' }
]
}
},
whitelabel: {
name: 'Whitelabel',
config: {
companyName: 'Custom Brand Status',
whitelabel: true,
customBranding: {
primaryColor: '#FF5722',
logoUrl: 'https://example.com/custom-logo.png',
footerText: 'Powered by Custom Infrastructure'
},
lastUpdated: Date.now(),
currentYear: new Date().getFullYear()
}
}
};
// Initial setup
let currentPreset = 'standard';
const applyPreset = (preset: any) => {
Object.keys(preset.config).forEach(key => {
footer[key] = preset.config[key];
});
updateConfigDisplay(preset.config);
};
applyPreset(configPresets[currentPreset]);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(configPresets).forEach(([key, preset]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentPreset ? ' active' : '');
button.textContent = preset.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentPreset = key;
footer.loading = true;
setTimeout(() => {
applyPreset(preset);
footer.loading = false;
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add configuration display
const configDisplay = document.createElement('div');
configDisplay.className = 'config-display';
wrapperElement.appendChild(configDisplay);
const updateConfigDisplay = (config: any) => {
configDisplay.innerHTML = Object.entries(config)
.filter(([key]) => key !== 'socialLinks' && key !== 'additionalLinks' && key !== 'languageOptions')
.map(([key, value]) => `
<div class="config-item">
<div class="config-label">${key}</div>
<div class="config-value">${value}</div>
</div>
`).join('');
};
// Handle events
footer.addEventListener('footerLinkClick', (event: CustomEvent) => {
console.log('Footer link clicked:', event.detail);
alert(`Link clicked: ${event.detail.type} - ${event.detail.url}`);
});
footer.addEventListener('subscribeClick', () => {
alert('Subscribe feature would open here');
});
footer.addEventListener('reportIncidentClick', () => {
alert('Report incident form would open here');
});
footer.addEventListener('languageChange', (event: CustomEvent) => {
alert(`Language changed to: ${event.detail.language}`);
});
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Loading and Error States -->
<div class="demo-section">
<div class="demo-title">Loading and Error States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Start with loading
footer.loading = true;
footer.companyName = 'LoadingCorp';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
<button class="demo-button" id="simulateOffline">Simulate Offline</button>
<button class="demo-button" id="brokenLinks">Broken Links</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
footer.loading = !footer.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
footer.loading = true;
setTimeout(() => {
footer.companyName = 'Successfully Loaded Inc';
footer.legalUrl = 'https://example.com/legal';
footer.supportEmail = 'support@loaded.com';
footer.statusPageUrl = 'https://status.loaded.com';
footer.lastUpdated = Date.now();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/loaded' },
{ platform: 'github', url: 'https://github.com/loaded' }
];
footer.loading = false;
footer.errorMessage = null;
}, 1000);
});
controls.querySelector('#simulateError')?.addEventListener('click', () => {
footer.loading = true;
setTimeout(() => {
footer.loading = false;
footer.errorMessage = 'Failed to load footer configuration';
footer.companyName = 'Error Loading';
footer.socialLinks = [];
}, 1500);
});
controls.querySelector('#simulateOffline')?.addEventListener('click', () => {
footer.offline = true;
footer.errorMessage = 'You are currently offline';
footer.lastUpdated = null;
});
controls.querySelector('#brokenLinks')?.addEventListener('click', () => {
footer.companyName = 'Broken Links Demo';
footer.legalUrl = 'https://broken.invalid/legal';
footer.supportEmail = 'invalid-email';
footer.socialLinks = [
{ platform: 'twitter', url: '' },
{ platform: 'github', url: 'not-a-url' }
];
footer.rssFeedUrl = 'https://broken.invalid/rss';
footer.apiStatusUrl = null;
});
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Test different loading states and error scenarios using the controls above.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Dynamic Updates and Real-time Features -->
<div class="demo-section">
<div class="demo-title">Dynamic Updates and Real-time Features</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Initial configuration
footer.companyName = 'RealTime Systems';
footer.legalUrl = 'https://realtime.com/legal';
footer.supportEmail = 'support@realtime.com';
footer.statusPageUrl = 'https://status.realtime.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
// Dynamic social links
const allSocialPlatforms = [
{ platform: 'twitter', url: 'https://twitter.com/realtime' },
{ platform: 'github', url: 'https://github.com/realtime' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/realtime' },
{ platform: 'facebook', url: 'https://facebook.com/realtime' },
{ platform: 'youtube', url: 'https://youtube.com/realtime' },
{ platform: 'instagram', url: 'https://instagram.com/realtime' },
{ platform: 'slack', url: 'https://realtime.slack.com' },
{ platform: 'discord', url: 'https://discord.gg/realtime' }
];
footer.socialLinks = allSocialPlatforms.slice(0, 3);
// Real-time status feed
footer.rssFeedUrl = 'https://status.realtime.com/rss';
footer.apiStatusUrl = 'https://api.realtime.com/v1/status';
// Status feed simulation
const statusUpdates = [
'All systems operational',
'Investigating API latency',
'Maintenance scheduled for tonight',
'Performance improvements deployed',
'New datacenter online',
'Security patch applied'
];
let updateIndex = 0;
footer.latestStatusUpdate = statusUpdates[0];
// Auto-update last updated time
const updateInterval = setInterval(() => {
footer.lastUpdated = Date.now();
}, 5000);
// Rotate status updates
const statusInterval = setInterval(() => {
updateIndex = (updateIndex + 1) % statusUpdates.length;
footer.latestStatusUpdate = statusUpdates[updateIndex];
logEvent(`Status update: ${statusUpdates[updateIndex]}`);
}, 8000);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addSocial">Add Social Link</button>
<button class="demo-button" id="removeSocial">Remove Social Link</button>
<button class="demo-button" id="updateStatus">Force Status Update</button>
<button class="demo-button" id="changeYear">Change Year</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#addSocial')?.addEventListener('click', () => {
if (footer.socialLinks.length < allSocialPlatforms.length) {
footer.socialLinks = [...footer.socialLinks, allSocialPlatforms[footer.socialLinks.length]];
logEvent(`Added ${allSocialPlatforms[footer.socialLinks.length - 1].platform} link`);
}
});
controls.querySelector('#removeSocial')?.addEventListener('click', () => {
if (footer.socialLinks.length > 0) {
const removed = footer.socialLinks[footer.socialLinks.length - 1];
footer.socialLinks = footer.socialLinks.slice(0, -1);
logEvent(`Removed ${removed.platform} link`);
}
});
controls.querySelector('#updateStatus')?.addEventListener('click', () => {
const customStatus = prompt('Enter custom status update:');
if (customStatus) {
footer.latestStatusUpdate = customStatus;
footer.lastUpdated = Date.now();
logEvent(`Custom status: ${customStatus}`);
}
});
controls.querySelector('#changeYear')?.addEventListener('click', () => {
footer.currentYear = footer.currentYear + 1;
logEvent(`Year changed to ${footer.currentYear}`);
});
// Event log
const eventLog = document.createElement('div');
eventLog.className = 'event-log';
eventLog.innerHTML = '<strong>Event Log:</strong><br>';
wrapperElement.appendChild(eventLog);
const logEvent = (message: string) => {
const time = new Date().toLocaleTimeString();
eventLog.innerHTML += `[${time}] ${message}<br>`;
eventLog.scrollTop = eventLog.scrollHeight;
};
logEvent('Real-time updates started');
// Cleanup
wrapperElement.addEventListener('remove', () => {
clearInterval(updateInterval);
clearInterval(statusInterval);
});
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Interactive Features -->
<div class="demo-section">
<div class="demo-title">Interactive Features and Actions</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Setup interactive footer
footer.companyName = 'Interactive Corp';
footer.legalUrl = 'https://interactive.com/legal';
footer.supportEmail = 'help@interactive.com';
footer.statusPageUrl = 'https://status.interactive.com';
footer.whitelabel = false;
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
// Interactive features
footer.enableSubscribe = true;
footer.enableReportIssue = true;
footer.enableLanguageSelector = true;
footer.enableThemeToggle = true;
footer.languageOptions = [
{ code: 'en', label: 'English' },
{ code: 'es', label: 'Español' },
{ code: 'fr', label: 'Français' },
{ code: 'de', label: 'Deutsch' },
{ code: 'ja', label: '日本語' },
{ code: 'zh', label: '中文' }
];
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/interactive' },
{ platform: 'github', url: 'https://github.com/interactive' },
{ platform: 'discord', url: 'https://discord.gg/interactive' }
];
footer.additionalLinks = [
{ label: 'API Documentation', url: 'https://docs.interactive.com' },
{ label: 'Service Level Agreement', url: 'https://interactive.com/sla' },
{ label: 'Privacy Policy', url: 'https://interactive.com/privacy' },
{ label: 'Terms of Service', url: 'https://interactive.com/terms' }
];
// Subscribe functionality
let subscriberCount = 1234;
footer.subscriberCount = subscriberCount;
footer.addEventListener('subscribeClick', (event: CustomEvent) => {
const email = prompt('Enter your email to subscribe:');
if (email && email.includes('@')) {
subscriberCount++;
footer.subscriberCount = subscriberCount;
logAction(`New subscriber: ${email} (Total: ${subscriberCount})`);
alert(`Successfully subscribed! You are subscriber #${subscriberCount}`);
}
});
// Report issue functionality
footer.addEventListener('reportIncidentClick', (event: CustomEvent) => {
const issue = prompt('Describe the issue you are experiencing:');
if (issue) {
const ticketId = `INC-${Date.now().toString().slice(-6)}`;
logAction(`Issue reported: ${ticketId} - ${issue.substring(0, 50)}...`);
alert(`Thank you! Your issue has been logged.\nTicket ID: ${ticketId}\nWe will investigate and update you at the provided email.`);
}
});
// Language change
footer.addEventListener('languageChange', (event: CustomEvent) => {
const newLang = event.detail.language;
footer.currentLanguage = newLang;
logAction(`Language changed to: ${newLang}`);
// Simulate translation
const translations = {
en: 'Interactive Corp',
es: 'Corporación Interactiva',
fr: 'Corp Interactif',
de: 'Interaktive GmbH',
ja: 'インタラクティブ株式会社',
zh: '互动公司'
};
footer.companyName = translations[newLang] || translations.en;
});
// Theme toggle
footer.addEventListener('themeToggle', (event: CustomEvent) => {
const theme = event.detail.theme;
logAction(`Theme changed to: ${theme}`);
footer.currentTheme = theme;
});
// Click tracking
footer.addEventListener('footerLinkClick', (event: CustomEvent) => {
logAction(`Link clicked: ${event.detail.type} - ${event.detail.label || event.detail.url}`);
});
// Action log
const actionLog = document.createElement('div');
actionLog.className = 'event-log';
actionLog.innerHTML = '<strong>User Actions:</strong><br>';
wrapperElement.appendChild(actionLog);
const logAction = (message: string) => {
const time = new Date().toLocaleTimeString();
actionLog.innerHTML += `[${time}] ${message}<br>`;
actionLog.scrollTop = actionLog.scrollHeight;
};
logAction('Interactive footer ready');
// Add info
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Try clicking on various footer elements to see the interactive features in action.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
const edgeCases = {
empty: {
name: 'Empty/Minimal',
config: {
companyName: '',
whitelabel: true,
lastUpdated: null
}
},
veryLong: {
name: 'Very Long Content',
config: {
companyName: 'International Mega Corporation with an Extremely Long Company Name That Tests Layout Limits Inc.',
legalUrl: 'https://very-long-domain-name-that-might-break-layouts.international-corporation.com/legal/terms-and-conditions/privacy-policy/cookie-policy',
supportEmail: 'customer.support.team@very-long-domain-name.international-corporation.com',
socialLinks: Array.from({ length: 15 }, (_, i) => ({
platform: ['twitter', 'github', 'linkedin', 'facebook', 'youtube'][i % 5],
url: `https://social-${i}.com/long-username-handle-that-tests-limits`
})),
additionalLinks: Array.from({ length: 10 }, (_, i) => ({
label: `Very Long Link Label That Might Cause Layout Issues #${i + 1}`,
url: `https://example.com/very/long/path/structure/that/goes/on/and/on/page-${i}`
}))
}
},
unicode: {
name: 'Unicode/International',
config: {
companyName: '🌍 全球服务 • グローバル • العالمية • Глобальный 🌏',
legalUrl: 'https://unicode.test/法律',
supportEmail: 'support@日本.jp',
currentYear: new Date().getFullYear(),
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/🌐' },
{ platform: 'github', url: 'https://github.com/世界' }
],
additionalLinks: [
{ label: '📋 Terms & Conditions', url: '#' },
{ label: '🔒 Privacy Policy', url: '#' },
{ label: '🛡️ Security', url: '#' }
]
}
},
brokenData: {
name: 'Broken/Invalid Data',
config: {
companyName: null,
legalUrl: 'not-a-valid-url',
supportEmail: 'not-an-email',
currentYear: 'not-a-year',
lastUpdated: 'invalid-timestamp',
socialLinks: [
{ platform: null, url: null },
{ platform: 'unknown-platform', url: '' },
{ url: 'https://missing-platform.com' },
{ platform: 'twitter' }
],
rssFeedUrl: '',
apiStatusUrl: undefined
}
},
maxData: {
name: 'Maximum Data',
config: {
companyName: 'Maximum Configuration Demo',
legalUrl: 'https://max.demo/legal',
supportEmail: 'all@max.demo',
statusPageUrl: 'https://status.max.demo',
whitelabel: false,
currentYear: new Date().getFullYear(),
lastUpdated: Date.now(),
language: 'en',
theme: 'dark',
socialLinks: Array.from({ length: 20 }, (_, i) => ({
platform: 'generic',
url: `https://social${i}.com`
})),
additionalLinks: Array.from({ length: 15 }, (_, i) => ({
label: `Link ${i + 1}`,
url: `#link${i + 1}`
})),
rssFeedUrl: 'https://status.max.demo/rss',
apiStatusUrl: 'https://api.max.demo/status',
subscriberCount: 999999,
enableSubscribe: true,
enableReportIssue: true,
enableLanguageSelector: true,
enableThemeToggle: true,
languageOptions: Array.from({ length: 50 }, (_, i) => ({
code: `lang${i}`,
label: `Language ${i}`
}))
}
}
};
// Initial setup
let currentCase = 'empty';
const applyCase = (edgeCase: any) => {
// Clear all properties first
Object.keys(footer).forEach(key => {
if (typeof footer[key] !== 'function') {
footer[key] = undefined;
}
});
// Apply new config
Object.keys(edgeCase.config).forEach(key => {
footer[key] = edgeCase.config[key];
});
};
applyCase(edgeCases[currentCase]);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(edgeCases).forEach(([key, edgeCase]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentCase ? ' active' : '');
button.textContent = edgeCase.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentCase = key;
applyCase(edgeCase);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add description
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = `
<strong>Edge Case Descriptions:</strong><br>
<strong>Empty:</strong> Minimal configuration with missing data<br>
<strong>Very Long:</strong> Tests layout with extremely long content<br>
<strong>Unicode:</strong> International characters and emojis<br>
<strong>Broken Data:</strong> Invalid or malformed configuration<br>
<strong>Maximum Data:</strong> All features with maximum content
`;
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-footer.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -10,18 +12,80 @@ declare global {
@customElement('upl-statuspage-footer')
export class UplStatuspageFooter extends DeesElement {
// STATIC
public static demo = () => html`
<upl-statuspage-footer></upl-statuspage-footer>
`;
public static demo = demoFunc;
// INSTANCE
@property()
public legalInfo: string = "https://lossless.gmbh";
@property({ type: String })
public companyName: string = '';
@property({
type: Boolean
})
public whitelabel = false;
@property({ type: String })
public legalUrl: string = '';
@property({ type: String })
public supportEmail: string = '';
@property({ type: String })
public statusPageUrl: string = '';
@property({ type: Boolean })
public whitelabel: boolean = false;
@property({ type: Number })
public lastUpdated: number | null = null;
@property({ type: Number })
public currentYear: number = new Date().getFullYear();
@property({ type: Array })
public socialLinks: Array<{ platform: string; url: string }> = [];
@property({ type: Array })
public additionalLinks: Array<{ label: string; url: string }> = [];
@property({ type: String })
public rssFeedUrl: string = '';
@property({ type: String })
public apiStatusUrl: string = '';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: String })
public errorMessage: string | null = null;
@property({ type: Boolean })
public offline: boolean = false;
@property({ type: String })
public latestStatusUpdate: string = '';
@property({ type: Boolean })
public enableSubscribe: boolean = false;
@property({ type: Boolean })
public enableReportIssue: boolean = false;
@property({ type: Boolean })
public enableLanguageSelector: boolean = false;
@property({ type: Boolean })
public enableThemeToggle: boolean = false;
@property({ type: Array })
public languageOptions: Array<{ code: string; label: string }> = [];
@property({ type: String })
public currentLanguage: string = 'en';
@property({ type: String })
public currentTheme: string = 'light';
@property({ type: Number })
public subscriberCount: number = 0;
@property({ type: Object })
public customBranding: { primaryColor?: string; logoUrl?: string; footerText?: string } | null = null;
constructor() {
@@ -30,43 +94,545 @@ export class UplStatuspageFooter extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
background: ${colors.background.primary};
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
font-size: 14px;
border-top: 1px solid ${colors.border.default};
}
.mainbox {
max-width: 900px;
margin: auto;
padding-top: 20px;
padding-bottom: 20px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: ${unsafeCSS(spacing['2xl'])} ${unsafeCSS(spacing.lg)};
}
.footer-content {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.xl)};
}
.footer-main {
display: grid;
grid-template-columns: 1fr auto;
gap: ${unsafeCSS(spacing['2xl'])};
align-items: start;
}
.company-info {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.lg)};
}
.company-name {
font-size: 20px;
font-weight: 600;
color: ${colors.text.primary};
}
.company-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(spacing.lg)};
}
.footer-link {
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
font-size: 14px;
}
.footer-link:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
.footer-actions {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.md)};
}
.action-button {
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
background: transparent;
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
text-align: center;
transition: all 0.2s ease;
white-space: nowrap;
font-size: 14px;
font-weight: 500;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.action-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.action-button:active {
transform: translateY(0);
}
.footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: ${unsafeCSS(spacing.xl)};
margin-top: ${unsafeCSS(spacing.xl)};
border-top: 1px solid ${colors.border.default};
}
.footer-meta {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
align-items: center;
flex-wrap: wrap;
}
.social-links {
display: flex;
gap: ${unsafeCSS(spacing.md)};
align-items: center;
}
.social-link {
display: inline-block;
width: 20px;
height: 20px;
opacity: 0.7;
transition: all 0.2s ease;
color: ${colors.text.secondary};
}
.social-link:hover {
opacity: 1;
color: ${colors.text.primary};
transform: translateY(-1px);
}
.social-link svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.copyright {
font-size: 14px;
color: ${colors.text.secondary};
}
.last-updated {
font-size: 13px;
color: ${colors.text.muted};
}
.powered-by {
font-size: 13px;
color: ${colors.text.muted};
text-align: right;
}
.powered-by a {
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
}
.powered-by a:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
.status-update {
padding: ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.lg)};
background: ${colors.background.muted};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
margin-bottom: ${unsafeCSS(spacing.lg)};
line-height: 1.6;
}
.language-selector {
position: relative;
}
.language-selector select {
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
border: 1px solid ${colors.border.default};
background: ${colors.background.primary};
color: ${colors.text.primary};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
cursor: pointer;
font-family: ${unsafeCSS(fonts.base)};
}
.theme-toggle {
cursor: pointer;
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
border: 1px solid ${colors.border.default};
background: transparent;
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
transition: all 0.2s ease;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.theme-toggle:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
}
.subscriber-count {
font-size: 12px;
color: ${colors.text.muted};
margin-top: ${unsafeCSS(spacing.xs)};
}
.error-message {
padding: ${unsafeCSS(spacing.md)};
background: ${cssManager.bdTheme('#fee9e9', '#7f1d1d')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
border-radius: ${unsafeCSS(borderRadius.base)};
margin-bottom: ${unsafeCSS(spacing.lg)};
font-size: 14px;
border: 1px solid ${cssManager.bdTheme('#fecaca', '#991b1b')};
}
.offline-indicator {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
background: ${colors.status.degraded};
color: white;
border-radius: ${unsafeCSS(borderRadius.full)};
font-size: 13px;
font-weight: 500;
}
.loading-skeleton {
height: 200px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.additional-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(spacing.md)};
margin-top: ${unsafeCSS(spacing.md)};
}
.additional-link {
font-size: 14px;
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
}
.additional-link:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
@media (max-width: 640px) {
.container {
padding: ${unsafeCSS(spacing.xl)} ${unsafeCSS(spacing.md)};
}
.footer-main {
grid-template-columns: 1fr;
gap: ${unsafeCSS(spacing.xl)};
}
.footer-bottom {
flex-direction: column;
gap: ${unsafeCSS(spacing.lg)};
align-items: start;
}
.footer-meta {
flex-direction: column;
align-items: start;
gap: ${unsafeCSS(spacing.md)};
}
.company-links {
flex-direction: column;
gap: ${unsafeCSS(spacing.sm)};
}
.powered-by {
text-align: left;
}
}
`
]
public render(): TemplateResult {
if (this.loading) {
return html`
${domtools.elementBasic.styles}
<style></style>
<div class="mainbox">
Hi there
<div class="container">
<div class="loading-skeleton"></div>
</div>
`;
}
public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', {
return html`
<div class="container">
<div class="footer-content">
${this.errorMessage ? html`
<div class="error-message">${this.errorMessage}</div>
` : ''}
}))
${this.offline ? html`
<div class="offline-indicator">
<span>⚠</span> You are currently offline
</div>
` : ''}
${this.latestStatusUpdate ? html`
<div class="status-update">
<strong>Latest Update:</strong> ${this.latestStatusUpdate}
</div>
` : ''}
<div class="footer-main">
<div class="company-info">
${this.companyName ? html`
<div class="company-name">${this.companyName}</div>
` : ''}
<div class="company-links">
${this.legalUrl && this.isValidUrl(this.legalUrl) ? html`
<a href="${this.legalUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'legal', this.legalUrl)}>Legal</a>
` : ''}
${this.supportEmail && this.isValidEmail(this.supportEmail) ? html`
<a href="mailto:${this.supportEmail}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'support', this.supportEmail)}>Support</a>
` : ''}
${this.statusPageUrl && this.isValidUrl(this.statusPageUrl) ? html`
<a href="${this.statusPageUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'status', this.statusPageUrl)}>Status Page</a>
` : ''}
${this.rssFeedUrl && this.isValidUrl(this.rssFeedUrl) ? html`
<a href="${this.rssFeedUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'rss', this.rssFeedUrl)}>RSS Feed</a>
` : ''}
${this.apiStatusUrl && this.isValidUrl(this.apiStatusUrl) ? html`
<a href="${this.apiStatusUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'api', this.apiStatusUrl)}>API Status</a>
` : ''}
</div>
${this.additionalLinks && this.additionalLinks.length > 0 ? html`
<div class="additional-links">
${this.additionalLinks.map(link => html`
${link.label && link.url && this.isValidUrl(link.url) ? html`
<a href="${link.url}" class="additional-link" @click=${(e: Event) => this.handleLinkClick(e, 'additional', link.url, link.label)}>
${link.label}
</a>
` : ''}
`)}
</div>
` : ''}
</div>
<div class="footer-actions">
${this.enableSubscribe ? html`
<button class="action-button" @click=${this.handleSubscribeClick}>
Subscribe to Updates
${this.subscriberCount > 0 ? html`
<div class="subscriber-count">${this.subscriberCount.toLocaleString()} subscribers</div>
` : ''}
</button>
` : ''}
${this.enableReportIssue ? html`
<button class="action-button" @click=${this.handleReportIncidentClick}>
Report an Issue
</button>
` : ''}
${this.enableLanguageSelector && this.languageOptions.length > 0 ? html`
<div class="language-selector">
<select @change=${this.handleLanguageChange} .value=${this.currentLanguage}>
${this.languageOptions.map(option => html`
<option value="${option.code}" ?selected=${option.code === this.currentLanguage}>
${option.label}
</option>
`)}
</select>
</div>
` : ''}
${this.enableThemeToggle ? html`
<button class="theme-toggle" @click=${this.handleThemeToggle}>
${this.currentTheme === 'dark' ? '☀️' : '🌙'} ${this.currentTheme === 'dark' ? 'Light' : 'Dark'} Mode
</button>
` : ''}
</div>
</div>
<div class="footer-bottom">
<div class="footer-meta">
<div class="copyright">
© ${this.currentYear} ${this.companyName || 'Status Page'}
</div>
${this.lastUpdated ? html`
<div class="last-updated">
Last updated: ${this.formatLastUpdated()}
</div>
` : ''}
${this.socialLinks && this.socialLinks.length > 0 ? html`
<div class="social-links">
${this.socialLinks.map(social => html`
${social.platform && social.url && this.isValidUrl(social.url) ? html`
<a href="${social.url}" class="social-link" title="${social.platform}" @click=${(e: Event) => this.handleLinkClick(e, 'social', social.url, social.platform)}>
${this.getSocialIcon(social.platform)}
</a>
` : ''}
`)}
</div>
` : ''}
</div>
${!this.whitelabel ? html`
<div class="powered-by">
${this.customBranding?.footerText || html`
Powered by <a href="https://uptime.link" target="_blank" @click=${(e: Event) => this.handleLinkClick(e, 'powered-by', 'https://uptime.link')}>uptime.link</a>
`}
</div>
` : ''}
</div>
</div>
</div>
`;
}
private isValidUrl(url: string): boolean {
if (!url) return false;
try {
new URL(url);
return true;
} catch {
return url.startsWith('#') || url.startsWith('/');
}
}
private isValidEmail(email: string): boolean {
if (!email) return false;
return email.includes('@');
}
private formatLastUpdated(): string {
if (!this.lastUpdated) return 'Never';
const date = new Date(this.lastUpdated);
const now = Date.now();
const diff = now - this.lastUpdated;
if (diff < 60000) {
return 'Just now';
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString();
}
}
private getSocialIcon(platform: string): TemplateResult {
const icons: Record<string, TemplateResult> = {
twitter: html`<svg viewBox="0 0 24 24"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>`,
github: html`<svg viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`,
linkedin: html`<svg viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>`,
facebook: html`<svg viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>`,
youtube: html`<svg viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`,
instagram: html`<svg viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zM5.838 12a6.162 6.162 0 1 1 12.324 0 6.162 6.162 0 0 1-12.324 0zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm4.965-10.405a1.44 1.44 0 1 1 2.881.001 1.44 1.44 0 0 1-2.881-.001z"/></svg>`,
slack: html`<svg viewBox="0 0 24 24"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>`,
discord: html`<svg viewBox="0 0 24 24"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>`,
generic: html`<svg viewBox="0 0 24 24"><path d="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z"/></svg>`
};
return icons[platform.toLowerCase()] || icons.generic;
}
private handleLinkClick(event: Event, type: string, url: string, label?: string) {
this.dispatchEvent(new CustomEvent('footerLinkClick', {
detail: { type, url, label },
bubbles: true,
composed: true
}));
}
private handleSubscribeClick() {
this.dispatchEvent(new CustomEvent('subscribeClick', {
bubbles: true,
composed: true
}));
}
private handleReportIncidentClick() {
this.dispatchEvent(new CustomEvent('reportIncidentClick', {
bubbles: true,
composed: true
}));
}
private handleLanguageChange(event: Event) {
const select = event.target as HTMLSelectElement;
const language = select.value;
this.currentLanguage = language;
this.dispatchEvent(new CustomEvent('languageChange', {
detail: { language },
bubbles: true,
composed: true
}));
}
private handleThemeToggle() {
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.currentTheme = newTheme;
this.dispatchEvent(new CustomEvent('themeToggle', {
detail: { theme: newTheme },
bubbles: true,
composed: true
}));
}
public dispatchReportNewIncident() {
this.handleReportIncidentClick();
}
public dispatchStatusSubscribe() {
this.dispatchEvent(new CustomEvent('statusSubscribe', {
}))
this.handleSubscribeClick();
}
}

View File

@@ -0,0 +1,241 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
</style>
<div class="demo-container">
<!-- Basic Header -->
<div class="demo-section">
<div class="demo-title">Basic Header with Dynamic Title</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
// Demo different titles
const titles = [
'MyService Status Page',
'Production Environment Status',
'API Health Dashboard',
'Global Infrastructure Status',
'🚀 Rocket Systems Monitor',
'Multi-Region Service Status'
];
let titleIndex = 0;
header.pageTitle = titles[titleIndex];
// Add event listeners
header.addEventListener('reportNewIncident', (event: CustomEvent) => {
console.log('Report incident clicked');
alert('Report Incident form would open here');
});
header.addEventListener('statusSubscribe', (event: CustomEvent) => {
console.log('Subscribe clicked');
alert('Subscribe modal would open here');
});
// Cycle through titles
setInterval(() => {
titleIndex = (titleIndex + 1) % titles.length;
header.pageTitle = titles[titleIndex];
}, 2000);
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Hidden Buttons -->
<div class="demo-section">
<div class="demo-title">Header with Configurable Buttons</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Configurable Button States';
// Add properties to control button visibility
header.showReportButton = true;
header.showSubscribeButton = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleReport">Toggle Report Button</button>
<button class="demo-button" id="toggleSubscribe">Toggle Subscribe Button</button>
<button class="demo-button" id="toggleBoth">Hide Both</button>
<button class="demo-button" id="showBoth">Show Both</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleReport')?.addEventListener('click', () => {
header.showReportButton = !header.showReportButton;
});
controls.querySelector('#toggleSubscribe')?.addEventListener('click', () => {
header.showSubscribeButton = !header.showSubscribeButton;
});
controls.querySelector('#toggleBoth')?.addEventListener('click', () => {
header.showReportButton = false;
header.showSubscribeButton = false;
});
controls.querySelector('#showBoth')?.addEventListener('click', () => {
header.showReportButton = true;
header.showSubscribeButton = true;
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Custom Styling -->
<div class="demo-section">
<div class="demo-title">Header with Custom Branding</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Enterprise Cloud Platform';
// Custom branding properties
header.brandColor = '#1976D2';
header.logoUrl = 'https://via.placeholder.com/120x40/1976D2/ffffff?text=LOGO';
header.customStyles = true;
// Simulate different brand states
const brands = [
{ title: 'Enterprise Cloud Platform', color: '#1976D2', logo: 'ENTERPRISE' },
{ title: 'StartUp SaaS Monitor', color: '#00BCD4', logo: 'STARTUP' },
{ title: 'Government Services Status', color: '#4CAF50', logo: 'GOV' },
{ title: 'Financial Systems Health', color: '#673AB7', logo: 'FINTECH' }
];
let brandIndex = 0;
setInterval(() => {
brandIndex = (brandIndex + 1) % brands.length;
const brand = brands[brandIndex];
header.pageTitle = brand.title;
header.brandColor = brand.color;
header.logoUrl = `https://via.placeholder.com/120x40/${brand.color.slice(1)}/ffffff?text=${brand.logo}`;
}, 3000);
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Loading State -->
<div class="demo-section">
<div class="demo-title">Header with Loading States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Loading State Demo';
header.loading = true;
// Simulate loading completion
setTimeout(() => {
header.loading = false;
header.pageTitle = 'Status Page Loaded';
}, 2000);
// Add loading toggle
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading State</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
header.loading = !header.loading;
if (header.loading) {
header.pageTitle = 'Loading...';
setTimeout(() => {
header.loading = false;
header.pageTitle = 'Status Page Ready';
}, 2000);
}
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Event Counter -->
<div class="demo-section">
<div class="demo-title">Header with Event Tracking</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Event Tracking Demo';
let reportCount = 0;
let subscribeCount = 0;
// Create counter display
const counterDisplay = document.createElement('div');
counterDisplay.style.marginTop = '16px';
counterDisplay.style.fontSize = '14px';
counterDisplay.innerHTML = `
<div>Report Clicks: <strong id="reportCount">0</strong></div>
<div>Subscribe Clicks: <strong id="subscribeCount">0</strong></div>
`;
wrapperElement.appendChild(counterDisplay);
header.addEventListener('reportNewIncident', () => {
reportCount++;
counterDisplay.querySelector('#reportCount').textContent = reportCount.toString();
console.log(`Report incident clicked ${reportCount} times`);
});
header.addEventListener('statusSubscribe', () => {
subscribeCount++;
counterDisplay.querySelector('#subscribeCount').textContent = subscribeCount.toString();
console.log(`Subscribe clicked ${subscribeCount} times`);
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-header.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -10,14 +12,30 @@ declare global {
@customElement('upl-statuspage-header')
export class UplStatuspageHeader extends DeesElement {
// STATIC
public static demo = () => html`
<upl-statuspage-header></upl-statuspage-header>
`;
public static demo = demoFunc;
// INSTANCE
@property()
@property({ type: String })
public pageTitle: string = "Statuspage Title";
@property({ type: Boolean })
public showReportButton: boolean = true;
@property({ type: Boolean })
public showSubscribeButton: boolean = true;
@property({ type: String })
public brandColor: string = '';
@property({ type: String })
public logoUrl: string = '';
@property({ type: Boolean })
public customStyles: boolean = false;
@property({ type: Boolean })
public loading: boolean = false;
constructor() {
super();
@@ -28,70 +46,162 @@ export class UplStatuspageHeader extends DeesElement {
css`
:host {
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
font-family: ${unsafeCSS(fonts.base)};
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
}
.mainbox {
margin: auto;
max-width: 900px;
max-width: 1200px;
padding: 0 24px;
}
.mainbox .actions {
display: flex;
justify-content: flex-end;
padding: 20px 0px 40px 0px;
gap: 8px;
padding: 24px 0;
}
.mainbox .actions .actionButton {
background: ${cssManager.bdTheme('#00000000', '#ffffff00')};
font-size: 12px;
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')};
padding: 6px 10px 7px 10px;
margin-left: 10px;
border-radius: 3px;
background: transparent;
font-size: 14px;
font-weight: 500;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.mainbox .actions .actionButton:hover {
background: ${cssManager.bdTheme('#333333', '#efefef')};
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')};
color: ${cssManager.bdTheme('#fff', '#333333')};
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
transform: translateY(-1px);
}
.mainbox .actions .actionButton:active {
transform: translateY(0);
}
.header-content {
padding: 48px 0 64px 0;
text-align: center;
}
h1 {
margin: 0px;
text-align: center;
font-weight: 600;
font-size: 35px;
margin: 0;
font-size: 48px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.1;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
h2 {
margin: 0px;
margin-top: 10px;
text-align: center;
font-weight: 600;
font-size: 18px;
.subtitle {
margin: 16px 0 0 0;
font-size: 16px;
font-weight: 400;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
letter-spacing: 0.02em;
text-transform: uppercase;
}
.logo {
display: block;
margin: 0 auto 32px;
max-width: 180px;
height: auto;
filter: ${cssManager.bdTheme('none', 'brightness(0) invert(1)')};
}
.loading-skeleton {
height: 200px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 6px;
margin: 24px 0;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Primary button variant */
.actionButton.primary {
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
color: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
border-color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.actionButton.primary:hover {
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
}
`
]
public render(): TemplateResult {
if (this.loading) {
return html`
<div class="mainbox">
<div class="loading-skeleton"></div>
</div>
`;
}
return html`
${domtools.elementBasic.styles}
<style>
${this.customStyles && this.brandColor ? `
.mainbox .actions .actionButton {
border-color: ${this.brandColor};
color: ${this.brandColor};
}
.mainbox .actions .actionButton:hover {
background: ${this.brandColor}10;
border-color: ${this.brandColor};
}
.mainbox .actions .actionButton.primary {
background: ${this.brandColor};
border-color: ${this.brandColor};
color: white;
}
.mainbox .actions .actionButton.primary:hover {
background: ${this.brandColor}dd;
border-color: ${this.brandColor}dd;
}
` : ''}
</style>
<div class="mainbox">
<div class="actions">
<div class="actionButton" @click=${this.dispatchReportNewIncident}>report new incident</div>
<div class="actionButton" @click=${this.dispatchStatusSubscribe}>subscribe</div>
${this.showReportButton ? html`
<div class="actionButton" @click=${this.dispatchReportNewIncident}>Report Issue</div>
` : ''}
${this.showSubscribeButton ? html`
<div class="actionButton primary" @click=${this.dispatchStatusSubscribe}>Subscribe</div>
` : ''}
</div>
<div class="header-content">
${this.logoUrl ? html`
<img src="${this.logoUrl}" alt="Logo" class="logo">
` : ''}
<h1>${this.pageTitle}</h1>
<h2>STATUS BOARD</h2>
<div class="subtitle">System Status</div>
</div>
</div>
`;
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,12 @@ import {
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import type { IIncidentDetails } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-incidents.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -18,73 +23,490 @@ declare global {
@customElement('upl-statuspage-incidents')
export class UplStatuspageIncidents extends DeesElement {
// STATIC
public static demo = () => html` <upl-statuspage-incidents></upl-statuspage-incidents> `;
public static demo = demoFunc;
// INSTANCE
@property({
type: Array,
})
public currentIncidences: plugins.uplInterfaces.data.IIncident[] = [];
public currentIncidents: IIncidentDetails[] = [];
@property({
type: Array,
})
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = [];
public pastIncidents: IIncidentDetails[] = [];
@property({
type: Boolean,
})
public whitelabel = false;
@property({
type: Boolean,
})
public loading = false;
@property({
type: Number,
})
public daysToShow = 90;
constructor() {
super();
}
public static styles = [
plugins.domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.mainbox {
max-width: 900px;
margin: auto;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.noIncidentBox {
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
padding: 10px;
margin-bottom: 15px;
border-radius: 3px;
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.xl)};
margin-bottom: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
text-align: center;
color: ${colors.text.secondary};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.incident-card {
background: ${colors.background.card};
border-radius: ${unsafeCSS(borderRadius.md)};
margin-bottom: ${unsafeCSS(spacing.lg)};
overflow: hidden;
box-shadow: ${unsafeCSS(shadows.sm)};
border: 1px solid ${colors.border.default};
transition: all 0.2s ease;
cursor: pointer;
}
.incident-card:hover {
box-shadow: ${unsafeCSS(shadows.md)};
transform: translateY(-2px);
}
.incident-header {
padding: ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
border-left: 4px solid;
display: flex;
align-items: start;
justify-content: space-between;
gap: ${unsafeCSS(spacing.md)};
}
.incident-header.critical {
border-left-color: ${colors.status.major};
}
.incident-header.major {
border-left-color: ${colors.status.partial};
}
.incident-header.minor {
border-left-color: ${colors.status.degraded};
}
.incident-header.maintenance {
border-left-color: ${colors.status.maintenance};
}
.incident-title {
font-size: 18px;
font-weight: 600;
margin: 0;
line-height: 1.3;
}
.incident-meta {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
margin-top: ${unsafeCSS(spacing.sm)};
font-size: 13px;
color: ${colors.text.secondary};
flex-wrap: wrap;
}
.incident-status {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.full)};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.incident-status.investigating {
background: ${cssManager.bdTheme('#fef3c7', '#78350f')};
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
}
.incident-status.identified {
background: ${cssManager.bdTheme('#e9d5ff', '#581c87')};
color: ${cssManager.bdTheme('#6b21a8', '#d8b4fe')};
}
.incident-status.monitoring {
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
}
.incident-status.resolved {
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
color: ${cssManager.bdTheme('#047857', '#6ee7b7')};
}
.incident-status.postmortem {
background: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
}
.incident-body {
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.incident-impact {
margin: ${unsafeCSS(spacing.md)} 0;
padding: ${unsafeCSS(spacing.md)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
line-height: 1.6;
}
.affected-services {
margin-top: ${unsafeCSS(spacing.md)};
}
.affected-services-title {
font-size: 13px;
font-weight: 600;
margin-bottom: ${unsafeCSS(spacing.sm)};
color: ${colors.text.primary};
}
.service-tag {
display: inline-block;
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
margin: 2px;
background: ${colors.background.muted};
border-radius: ${unsafeCSS(borderRadius.sm)};
font-size: 12px;
color: ${colors.text.secondary};
}
.incident-updates {
margin-top: ${unsafeCSS(spacing.lg)};
border-top: 1px solid ${colors.border.default};
padding-top: ${unsafeCSS(spacing.lg)};
}
.update-item {
position: relative;
padding-left: ${unsafeCSS(spacing.lg)};
margin-bottom: ${unsafeCSS(spacing.md)};
}
.update-item::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(borderRadius.full)};
background: ${colors.border.muted};
}
.update-time {
font-size: 12px;
color: ${colors.text.secondary};
margin-bottom: ${unsafeCSS(spacing.xs)};
font-family: ${unsafeCSS(fonts.mono)};
}
.update-message {
font-size: 14px;
line-height: 1.6;
color: ${colors.text.primary};
}
.update-author {
font-size: 12px;
color: ${colors.text.secondary};
margin-top: ${unsafeCSS(spacing.xs)};
font-style: italic;
}
.loading-skeleton {
height: 140px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
margin-bottom: ${unsafeCSS(spacing.lg)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.show-more {
text-align: center;
margin-top: ${unsafeCSS(spacing.lg)};
}
.show-more-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.lg)};
background: transparent;
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.show-more-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.show-more-button:active {
transform: translateY(0);
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.incident-header {
flex-direction: column;
align-items: start;
gap: ${unsafeCSS(spacing.sm)};
}
.incident-meta {
flex-direction: column;
gap: ${unsafeCSS(spacing.xs)};
}
}
`,
];
public render(): TemplateResult {
return html`
<style></style>
<div class="mainbox">
<uplinternal-miniheading> Current Incidents </uplinternal-miniheading>
${this.currentIncidences.length
? html``
: html` <div class="noIncidentBox">No incidents ongoing.</div> `}
<uplinternal-miniheading> Past Incidents </uplinternal-miniheading>
${this.pastIncidences.length
? html``
: html` <div class="noIncidentBox">No past incidents in the last 90 days.</div> `}
<div class="container">
<uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
${this.loading ? html`
<div class="loading-skeleton"></div>
` : this.currentIncidents.length ?
this.currentIncidents.map(incident => this.renderIncident(incident, true)) :
html`<div class="noIncidentBox">No incidents ongoing.</div>`
}
<uplinternal-miniheading>Past Incidents</uplinternal-miniheading>
${this.loading ? html`
<div class="loading-skeleton"></div>
<div class="loading-skeleton"></div>
` : this.pastIncidents.length ?
this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) :
html`<div class="noIncidentBox">No past incidents in the last ${this.daysToShow} days.</div>`
}
${this.pastIncidents.length > 5 && !this.loading ? html`
<div class="show-more">
<button class="show-more-button" @click=${this.handleShowMore}>
Show ${this.pastIncidents.length - 5} more incidents
</button>
</div>
` : ''}
</div>
`;
}
private renderIncident(incident: IIncidentDetails, isCurrent: boolean): TemplateResult {
const latestUpdate = incident.updates[incident.updates.length - 1];
const duration = incident.endTime ?
this.formatDuration(incident.endTime - incident.startTime) :
this.formatDuration(Date.now() - incident.startTime);
return html`
<div class="incident-card" @click=${() => this.handleIncidentClick(incident)}>
<div class="incident-header ${incident.severity}">
<div>
<h3 class="incident-title">${incident.title}</h3>
<div class="incident-meta">
<span>Started: ${this.formatDate(incident.startTime)}</span>
<span>Duration: ${duration}</span>
${incident.endTime ? html`
<span>Ended: ${this.formatDate(incident.endTime)}</span>
` : ''}
</div>
</div>
<div class="incident-status ${latestUpdate.status}">
${this.getStatusIcon(latestUpdate.status)}
${latestUpdate.status.replace(/_/g, ' ')}
</div>
</div>
<div class="incident-body">
<div class="incident-impact">
<strong>Impact:</strong> ${incident.impact}
</div>
${incident.affectedServices.length > 0 ? html`
<div class="affected-services">
<div class="affected-services-title">Affected Services:</div>
${incident.affectedServices.map(service => html`
<span class="service-tag">${service}</span>
`)}
</div>
` : ''}
${incident.updates.length > 0 ? html`
<div class="incident-updates">
<h4 style="font-size: 14px; margin: 0 0 12px 0;">Updates</h4>
${incident.updates.slice(-3).reverse().map(update => this.renderUpdate(update))}
</div>
` : ''}
${incident.rootCause && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Root Cause:</strong> ${incident.rootCause}
</div>
` : ''}
${incident.resolution && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Resolution:</strong> ${incident.resolution}
</div>
` : ''}
</div>
</div>
`;
}
private renderUpdate(update: any): TemplateResult {
return html`
<div class="update-item">
<div class="update-time">${this.formatDate(update.timestamp)}</div>
<div class="update-message">${update.message}</div>
${update.author ? html`
<div class="update-author">— ${update.author}</div>
` : ''}
</div>
`;
}
private getStatusIcon(status: string): string {
const icons: Record<string, string> = {
investigating: '🔍',
identified: '🎯',
monitoring: '👁️',
resolved: '✅',
postmortem: '📋'
};
return icons[status] || '•';
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
const now = Date.now();
const diff = now - timestamp;
// Less than 1 hour ago
if (diff < 60 * 60 * 1000) {
const minutes = Math.floor(diff / (60 * 1000));
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
}
// Less than 24 hours ago
if (diff < 24 * 60 * 60 * 1000) {
const hours = Math.floor(diff / (60 * 60 * 1000));
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
}
// Less than 7 days ago
if (diff < 7 * 24 * 60 * 60 * 1000) {
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
return `${days} day${days !== 1 ? 's' : ''} ago`;
}
// Default to full date
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
});
}
private formatDuration(milliseconds: number): string {
const minutes = Math.floor(milliseconds / (60 * 1000));
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else {
return `${minutes}m`;
}
}
private handleIncidentClick(incident: IIncidentDetails) {
this.dispatchEvent(new CustomEvent('incidentClick', {
detail: { incident },
bubbles: true,
composed: true
}));
}
private handleShowMore() {
// This would typically load more incidents or navigate to a full list
console.log('Show more incidents');
}
public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', {}));
this.dispatchEvent(new CustomEvent('reportNewIncident', {
bubbles: true,
composed: true
}));
}
public dispatchStatusSubscribe() {
this.dispatchEvent(new CustomEvent('statusSubscribe', {}));
this.dispatchEvent(new CustomEvent('statusSubscribe', {
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,393 @@
import { html } from '@design.estate/dees-element';
import type { IOverallStatus } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.status-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
</style>
<div class="demo-container">
<!-- Cycling Through All States -->
<div class="demo-section">
<div class="demo-title">Automatic Status Cycling</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statusStates: IOverallStatus[] = [
{
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 12
},
{
status: 'degraded',
message: 'Minor Service Degradation',
lastUpdated: Date.now(),
affectedServices: 2,
totalServices: 12
},
{
status: 'partial_outage',
message: 'Partial System Outage',
lastUpdated: Date.now(),
affectedServices: 4,
totalServices: 12
},
{
status: 'major_outage',
message: 'Major Service Disruption',
lastUpdated: Date.now(),
affectedServices: 8,
totalServices: 12
},
{
status: 'maintenance',
message: 'Scheduled Maintenance in Progress',
lastUpdated: Date.now(),
affectedServices: 3,
totalServices: 12
}
];
let statusIndex = 0;
// Initial loading demo
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = statusStates[0];
}, 1500);
// Cycle through states
setInterval(() => {
statusIndex = (statusIndex + 1) % statusStates.length;
statusBar.overallStatus = statusStates[statusIndex];
statusBar.overallStatus = { ...statusBar.overallStatus, lastUpdated: Date.now() };
}, 3000);
// Handle clicks
statusBar.addEventListener('statusClick', (event: CustomEvent) => {
console.log('Status bar clicked:', event.detail);
alert(`Status Details:\n\nStatus: ${event.detail.status.status}\nMessage: ${event.detail.status.message}\nAffected Services: ${event.detail.status.affectedServices}`);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Manual Status Control -->
<div class="demo-section">
<div class="demo-title">Manual Status Control</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Initial state
statusBar.overallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 15
};
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" data-status="operational">Operational</button>
<button class="demo-button" data-status="degraded">Degraded</button>
<button class="demo-button" data-status="partial_outage">Partial Outage</button>
<button class="demo-button" data-status="major_outage">Major Outage</button>
<button class="demo-button" data-status="maintenance">Maintenance</button>
`;
wrapperElement.appendChild(controls);
// Status messages
const statusMessages = {
operational: 'All Systems Operational',
degraded: 'Performance Issues Detected',
partial_outage: 'Some Services Unavailable',
major_outage: 'Critical System Failure',
maintenance: 'Planned Maintenance Window'
};
const affectedCounts = {
operational: 0,
degraded: 3,
partial_outage: 7,
major_outage: 12,
maintenance: 5
};
// Handle button clicks
controls.querySelectorAll('.demo-button').forEach(button => {
button.addEventListener('click', () => {
const status = button.getAttribute('data-status') as keyof typeof statusMessages;
statusBar.overallStatus = {
status: status as any,
message: statusMessages[status],
lastUpdated: Date.now(),
affectedServices: affectedCounts[status],
totalServices: 15
};
});
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Loading States -->
<div class="demo-section">
<div class="demo-title">Loading and Refresh States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Initial loading
statusBar.loading = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="refresh">Refresh Status</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
`;
wrapperElement.appendChild(controls);
// Set initial status after loading
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 10
};
}, 2000);
// Toggle loading
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusBar.loading = !statusBar.loading;
});
// Refresh simulation
controls.querySelector('#refresh')?.addEventListener('click', () => {
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
// Simulate random status after refresh
const statuses = ['operational', 'degraded', 'partial_outage'];
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
statusBar.overallStatus = {
status: randomStatus as any,
message: 'Status refreshed at ' + new Date().toLocaleTimeString(),
lastUpdated: Date.now(),
affectedServices: randomStatus === 'operational' ? 0 : Math.floor(Math.random() * 5) + 1,
totalServices: 10
};
}, 1000);
});
// Error simulation
controls.querySelector('#simulateError')?.addEventListener('click', () => {
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = {
status: 'major_outage',
message: 'Unable to fetch status - Connection Error',
lastUpdated: Date.now(),
affectedServices: -1, // Unknown
totalServices: -1
};
}, 1500);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const edgeCases = [
{
label: 'No Services',
status: {
status: 'operational',
message: 'No services to monitor',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 0
}
},
{
label: 'All Services Down',
status: {
status: 'major_outage',
message: 'Complete System Failure',
lastUpdated: Date.now(),
affectedServices: 25,
totalServices: 25
}
},
{
label: 'Very Long Message',
status: {
status: 'degraded',
message: 'Multiple services experiencing degraded performance due to increased load from seasonal traffic surge',
lastUpdated: Date.now(),
affectedServices: 7,
totalServices: 20
}
},
{
label: 'Old Timestamp',
status: {
status: 'operational',
message: 'Status data may be stale',
lastUpdated: Date.now() - 24 * 60 * 60 * 1000, // 24 hours ago
affectedServices: 0,
totalServices: 10
}
},
{
label: 'Future Maintenance',
status: {
status: 'maintenance',
message: 'Scheduled maintenance starting in 2 hours',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 15
}
}
];
let currentCase = 0;
statusBar.overallStatus = edgeCases[0].status;
// Create info display
const info = document.createElement('div');
info.className = 'status-info';
info.innerHTML = `<strong>Current Case:</strong> ${edgeCases[0].label}`;
wrapperElement.appendChild(info);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="prevCase"> Previous Case</button>
<button class="demo-button" id="nextCase">Next Case </button>
`;
wrapperElement.appendChild(controls);
const updateCase = (index: number) => {
currentCase = index;
statusBar.overallStatus = edgeCases[currentCase].status;
info.innerHTML = `<strong>Current Case:</strong> ${edgeCases[currentCase].label}`;
};
controls.querySelector('#prevCase')?.addEventListener('click', () => {
const newIndex = (currentCase - 1 + edgeCases.length) % edgeCases.length;
updateCase(newIndex);
});
controls.querySelector('#nextCase')?.addEventListener('click', () => {
const newIndex = (currentCase + 1) % edgeCases.length;
updateCase(newIndex);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Non-Expandable Status Bar -->
<div class="demo-section">
<div class="demo-title">Non-Expandable Status Bar</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Disable expandable behavior
statusBar.expandable = false;
statusBar.overallStatus = {
status: 'operational',
message: 'This status bar cannot be clicked',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 8
};
// This event won't fire since expandable is false
statusBar.addEventListener('statusClick', (event: CustomEvent) => {
console.log('This should not fire');
});
const info = document.createElement('div');
info.className = 'status-info';
info.innerHTML = 'Try clicking the status bar - it should not respond to clicks when expandable=false';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,8 @@
import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IOverallStatus } from '../interfaces/index.js';
import { fonts } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-statusbar.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -9,9 +12,22 @@ declare global {
@customElement('upl-statuspage-statusbar')
export class UplStatuspageStatusbar extends DeesElement {
public static demo = () => html`
<upl-statuspage-statusbar></upl-statuspage-statusbar>
`;
public static demo = demoFunc;
@property({ type: Object })
public overallStatus: IOverallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 0
};
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Boolean })
public expandable: boolean = true;
constructor() {
super();
@@ -21,30 +37,200 @@ export class UplStatuspageStatusbar extends DeesElement {
cssManager.defaultStyles,
css`
:host {
padding: 20px 0px 15px 0px;
padding: 0;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
}
.mainbox {
.statusbar-container {
margin: auto;
max-width: 900px;
text-align: center;
background: #19572E;
line-height: 50px;
border-radius: 3px;
max-width: 1200px;
padding: 0 24px 24px 24px;
position: relative;
}
.statusbar-inner {
display: flex;
align-items: center;
justify-content: center;
min-height: 64px;
padding: 16px 24px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
font-weight: 500;
font-size: 16px;
border: 1px solid transparent;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
}
.statusbar-inner:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
}
.statusbar-inner:active {
transform: translateY(0);
}
.statusbar-inner.operational {
background: ${cssManager.bdTheme('#10b981', '#064e3b')};
border-color: ${cssManager.bdTheme('#10b981', '#064e3b')};
color: white;
}
.statusbar-inner.degraded {
background: ${cssManager.bdTheme('#f59e0b', '#78350f')};
border-color: ${cssManager.bdTheme('#f59e0b', '#78350f')};
color: white;
}
.statusbar-inner.partial_outage {
background: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
border-color: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
color: white;
}
.statusbar-inner.major_outage {
background: ${cssManager.bdTheme('#dc2626', '#450a0a')};
border-color: ${cssManager.bdTheme('#dc2626', '#450a0a')};
color: white;
}
.statusbar-inner.maintenance {
background: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
border-color: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
color: white;
}
.status-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.status-main {
display: flex;
align-items: center;
gap: 8px;
}
.status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 14px;
}
.loading-skeleton {
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
height: 64px;
border-radius: 8px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.status-details {
font-size: 14px;
opacity: 0.9;
}
.last-updated {
font-size: 13px;
text-align: right;
margin-top: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
@media (max-width: 640px) {
.statusbar-inner {
font-size: 14px;
padding: 12px 16px;
min-height: 56px;
}
.status-icon {
width: 18px;
height: 18px;
font-size: 12px;
}
}
`,
]
public render(): TemplateResult {
const getStatusIcon = () => {
switch (this.overallStatus.status) {
case 'operational':
return '✓';
case 'degraded':
return '!';
case 'partial_outage':
return '⚠';
case 'major_outage':
return '✕';
case 'maintenance':
return '🔧';
default:
return '';
}
};
const formatLastUpdated = () => {
const date = new Date(this.overallStatus.lastUpdated);
return date.toLocaleString();
};
const handleClick = () => {
if (this.expandable) {
this.dispatchEvent(new CustomEvent('statusClick', {
detail: { status: this.overallStatus },
bubbles: true,
composed: true
}));
}
};
return html`
<style>
</style>
<div class="mainbox">
Everything is working normally!
<div class="statusbar-container">
${this.loading ? html`
<div class="loading-skeleton"></div>
` : html`
<div class="statusbar-inner ${this.overallStatus.status}" @click=${handleClick}>
<div class="status-content">
<div class="status-main">
<span class="status-icon">${getStatusIcon()}</span>
<span>${this.overallStatus.message}</span>
</div>
${this.overallStatus.affectedServices > 0 ? html`
<div class="status-details">
${this.overallStatus.affectedServices} of ${this.overallStatus.totalServices} services affected
</div>
` : ''}
</div>
</div>
<div class="last-updated">
Last updated: ${formatLastUpdated()}
</div>
`}
</div>
`;
}

View File

@@ -0,0 +1,754 @@
import { html } from '@design.estate/dees-element';
import type { IStatusHistoryPoint } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-box {
background: white;
padding: 12px;
border-radius: 4px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #2196F3;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 4px;
}
</style>
<div class="demo-container">
<!-- Time Range Demo -->
<div class="demo-section">
<div class="demo-title">Different Time Ranges</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Generate data for different time ranges
const generateDataForRange = (hours: number, pattern: 'stable' | 'degrading' | 'improving' | 'volatile' = 'stable'): IStatusHistoryPoint[] => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
// For proper display, we need hourly data points that align with actual hours
for (let i = hours - 1; i >= 0; i--) {
// Create timestamp at the start of each hour
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
const timestamp = date.getTime();
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 50;
let errorRate = 0;
switch (pattern) {
case 'degrading':
// Getting worse over time
const degradation = (hours - i) / hours;
if (degradation > 0.7) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (degradation > 0.5) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (degradation > 0.3) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
}
break;
case 'improving':
// Getting better over time
const improvement = i / hours;
if (improvement < 0.3) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (improvement < 0.5) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (improvement < 0.7) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
}
break;
case 'volatile':
// Random ups and downs
const rand = Math.random();
if (rand < 0.05) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (rand < 0.1) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (rand < 0.2) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
} else if (rand < 0.25) {
status = 'maintenance';
responseTime = 100 + Math.random() * 50;
errorRate = 0;
}
break;
default:
// Stable with occasional hiccups
if (Math.random() < 0.02) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.01 + Math.random() * 0.02;
}
}
data.push({
timestamp,
status,
responseTime,
errorRate
});
}
return data;
};
// Initial setup
statusDetails.serviceId = 'api-gateway';
statusDetails.serviceName = 'API Gateway';
statusDetails.historyData = generateDataForRange(24);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const timeRanges = [
{ hours: 24, label: '24 Hours' },
{ hours: 168, label: '7 Days' },
{ hours: 720, label: '30 Days' },
{ hours: 2160, label: '90 Days' }
];
timeRanges.forEach((range, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = range.label;
button.onclick = () => {
// Update active button
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Load new data with loading state
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = generateDataForRange(range.hours, 'volatile');
statusDetails.loading = false;
updateStats();
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add statistics display
const statsDiv = document.createElement('div');
statsDiv.className = 'stats-grid';
wrapperElement.appendChild(statsDiv);
const updateStats = () => {
const data = statusDetails.historyData || [];
const operational = data.filter(d => d.status === 'operational').length;
const avgResponseTime = data.reduce((sum, d) => sum + (d.responseTime || 0), 0) / data.length;
const uptime = (operational / data.length) * 100;
const incidents = data.filter(d => d.status !== 'operational' && d.status !== 'maintenance').length;
statsDiv.innerHTML = `
<div class="stat-box">
<div class="stat-value">${uptime.toFixed(2)}%</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat-box">
<div class="stat-value">${avgResponseTime.toFixed(0)}ms</div>
<div class="stat-label">Avg Response Time</div>
</div>
<div class="stat-box">
<div class="stat-value">${incidents}</div>
<div class="stat-label">Incidents</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.length}</div>
<div class="stat-label">Data Points</div>
</div>
`;
};
updateStats();
// Handle bar clicks
statusDetails.addEventListener('barClick', (event: CustomEvent) => {
const { timestamp, status, responseTime, errorRate } = event.detail;
const date = new Date(timestamp);
alert(`Details for ${date.toLocaleString()}:\n\nStatus: ${status}\nResponse Time: ${responseTime.toFixed(0)}ms\nError Rate: ${(errorRate * 100).toFixed(2)}%`);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Data Pattern Scenarios -->
<div class="demo-section">
<div class="demo-title">Different Data Patterns</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Pattern generators
const patterns = {
stable: () => {
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
data.push({
timestamp: date.getTime(),
status: 'operational',
responseTime: 40 + Math.random() * 20,
errorRate: 0
});
}
return data;
},
degrading: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const degradation = (47 - i) / 47;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50;
let errorRate = 0;
if (degradation > 0.8) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.4;
} else if (degradation > 0.6) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 100;
errorRate = 0.2;
} else if (degradation > 0.4) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.05;
} else {
responseTime = 50 + degradation * 100;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
recovering: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const recovery = i / 47;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50;
let errorRate = 0;
if (recovery < 0.2) {
status = 'operational';
responseTime = 50 + Math.random() * 20;
} else if (recovery < 0.4) {
status = 'degraded';
responseTime = 150 + Math.random() * 50;
errorRate = 0.02;
} else if (recovery < 0.7) {
status = 'partial_outage';
responseTime = 400 + Math.random() * 100;
errorRate = 0.15;
} else {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.35;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
periodic: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
// Issues every 12 hours
const hourOfDay = i % 24;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 30;
let errorRate = 0;
if (hourOfDay >= 9 && hourOfDay <= 11) {
// Morning peak
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.05;
} else if (hourOfDay >= 18 && hourOfDay <= 20) {
// Evening peak
status = 'degraded';
responseTime = 250 + Math.random() * 150;
errorRate = 0.08;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
maintenance: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 30;
let errorRate = 0;
// Maintenance window from hour 20-24
if (i >= 20 && i <= 24) {
status = 'maintenance';
responseTime = 0;
errorRate = 0;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
}
};
// Initial setup
statusDetails.serviceId = 'web-server';
statusDetails.serviceName = 'Web Server';
statusDetails.historyData = patterns.stable();
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(patterns).forEach(([name, generator]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (name === 'stable' ? ' active' : '');
button.textContent = name.charAt(0).toUpperCase() + name.slice(1);
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = generator();
statusDetails.loading = false;
updateInfo(name);
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
wrapperElement.appendChild(info);
const updateInfo = (pattern: string) => {
const descriptions = {
stable: 'Service running smoothly with consistent performance',
degrading: 'Service health deteriorating over time',
recovering: 'Service recovering from a major outage',
periodic: 'Regular performance issues during peak hours (9-11 AM and 6-8 PM)',
maintenance: 'Scheduled maintenance window (hours 20-24)'
};
info.innerHTML = `<strong>Pattern:</strong> ${descriptions[pattern as keyof typeof descriptions] || pattern}`;
};
updateInfo('stable');
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Interactive Real-time Updates -->
<div class="demo-section">
<div class="demo-title">Real-time Updates with Manual Control</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Initialize with recent data
const now = Date.now();
const initialData: IStatusHistoryPoint[] = [];
for (let i = 23; i >= 0; i--) {
initialData.push({
timestamp: now - (i * 60 * 60 * 1000),
status: 'operational',
responseTime: 50 + Math.random() * 30,
errorRate: 0
});
}
statusDetails.serviceId = 'real-time-api';
statusDetails.serviceName = 'Real-time API';
statusDetails.historyData = initialData;
statusDetails.timeRange = '24h';
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addHealthy">Add Healthy Point</button>
<button class="demo-button" id="addDegraded">Add Degraded Point</button>
<button class="demo-button" id="addOutage">Add Outage Point</button>
<button class="demo-button" id="simulateSpike">Simulate Traffic Spike</button>
<button class="demo-button" id="clearData">Clear All Data</button>
`;
wrapperElement.appendChild(controls);
const addDataPoint = (status: IStatusHistoryPoint['status'], responseTime: number, errorRate: number = 0) => {
const data = [...(statusDetails.historyData || [])];
if (data.length >= 24) {
data.shift(); // Keep only 24 points
}
data.push({
timestamp: Date.now(),
status,
responseTime,
errorRate
});
statusDetails.historyData = data;
};
controls.querySelector('#addHealthy')?.addEventListener('click', () => {
addDataPoint('operational', 50 + Math.random() * 30);
});
controls.querySelector('#addDegraded')?.addEventListener('click', () => {
addDataPoint('degraded', 200 + Math.random() * 100, 0.05);
});
controls.querySelector('#addOutage')?.addEventListener('click', () => {
addDataPoint('major_outage', 800 + Math.random() * 200, 0.5);
});
controls.querySelector('#simulateSpike')?.addEventListener('click', () => {
// Add several degraded points
for (let i = 0; i < 3; i++) {
setTimeout(() => {
addDataPoint('degraded', 300 + Math.random() * 200, 0.1 + Math.random() * 0.1);
}, i * 1000);
}
});
controls.querySelector('#clearData')?.addEventListener('click', () => {
statusDetails.historyData = [];
});
// Auto-update every 5 seconds
let autoUpdate = setInterval(() => {
const rand = Math.random();
if (rand < 0.8) {
addDataPoint('operational', 40 + Math.random() * 40);
} else if (rand < 0.95) {
addDataPoint('degraded', 150 + Math.random() * 100, 0.02);
} else {
addDataPoint('partial_outage', 400 + Math.random() * 200, 0.15);
}
}, 5000);
// Add toggle for auto-updates
const autoToggle = document.createElement('button');
autoToggle.className = 'demo-button active';
autoToggle.textContent = 'Auto-update: ON';
autoToggle.style.marginLeft = '10px';
autoToggle.onclick = () => {
if (autoUpdate) {
clearInterval(autoUpdate);
autoUpdate = null;
autoToggle.textContent = 'Auto-update: OFF';
autoToggle.classList.remove('active');
} else {
autoUpdate = setInterval(() => {
const rand = Math.random();
if (rand < 0.8) {
addDataPoint('operational', 40 + Math.random() * 40);
} else if (rand < 0.95) {
addDataPoint('degraded', 150 + Math.random() * 100, 0.02);
} else {
addDataPoint('partial_outage', 400 + Math.random() * 200, 0.15);
}
}, 5000);
autoToggle.textContent = 'Auto-update: ON';
autoToggle.classList.add('active');
}
};
controls.appendChild(autoToggle);
// Cleanup on unmount
wrapperElement.addEventListener('remove', () => {
if (autoUpdate) clearInterval(autoUpdate);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const scenarios = {
noData: {
name: 'No Data Available',
data: []
},
singlePoint: {
name: 'Single Data Point',
data: [{
timestamp: Date.now(),
status: 'operational' as const,
responseTime: 75,
errorRate: 0
}]
},
allDown: {
name: 'Complete Outage',
data: Array.from({ length: 48 }, (_, i) => ({
timestamp: Date.now() - (i * 60 * 60 * 1000),
status: 'major_outage' as const,
responseTime: 0,
errorRate: 1
}))
},
highLatency: {
name: 'High Latency Issues',
data: Array.from({ length: 48 }, (_, i) => ({
timestamp: Date.now() - (i * 60 * 60 * 1000),
status: 'operational' as const,
responseTime: 2000 + Math.random() * 1000,
errorRate: 0
}))
},
mixedStatuses: {
name: 'All Status Types',
data: Array.from({ length: 50 }, (_, i) => {
const statuses: IStatusHistoryPoint['status'][] = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
const status = statuses[i % statuses.length];
return {
timestamp: Date.now() - (i * 60 * 60 * 1000),
status,
responseTime: status === 'operational' ? 50 : status === 'maintenance' ? 0 : 200 + Math.random() * 600,
errorRate: status === 'operational' || status === 'maintenance' ? 0 : 0.1 + Math.random() * 0.4
};
})
}
};
// Initial scenario
let currentScenario = 'noData';
statusDetails.serviceId = 'edge-case-service';
statusDetails.serviceName = 'Edge Case Service';
statusDetails.historyData = scenarios[currentScenario].data;
// Create scenario buttons
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(scenarios).forEach(([key, scenario]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentScenario ? ' active' : '');
button.textContent = scenario.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentScenario = key;
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = scenario.data;
statusDetails.loading = false;
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Loading and Error States -->
<div class="demo-section">
<div class="demo-title">Loading and Error Handling</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Start with loading
statusDetails.loading = true;
statusDetails.serviceId = 'loading-demo';
statusDetails.serviceName = 'Loading Demo Service';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
<button class="demo-button" id="loadError">Simulate Error</button>
<button class="demo-button" id="loadSlowly">Load Slowly (3s)</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusDetails.loading = !statusDetails.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
const now = Date.now();
statusDetails.historyData = Array.from({ length: 24 }, (_, i) => ({
timestamp: now - (i * 60 * 60 * 1000),
status: Math.random() > 0.9 ? 'degraded' : 'operational',
responseTime: 50 + Math.random() * 50,
errorRate: 0
}));
statusDetails.loading = false;
}, 500);
});
controls.querySelector('#loadError')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
statusDetails.loading = false;
statusDetails.historyData = [];
statusDetails.errorMessage = 'Failed to load status data: Connection timeout';
}, 1500);
});
controls.querySelector('#loadSlowly')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
const now = Date.now();
statusDetails.historyData = Array.from({ length: 48 }, (_, i) => ({
timestamp: now - (i * 60 * 60 * 1000),
status: 'operational',
responseTime: 45 + Math.random() * 30,
errorRate: 0
}));
statusDetails.loading = false;
}, 3000);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -7,9 +7,13 @@ import {
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import type { IStatusHistoryPoint } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusdetails.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,7 +23,22 @@ declare global {
@customElement('upl-statuspage-statusdetails')
export class UplStatuspageStatusdetails extends DeesElement {
public static demo = () => html` <upl-statuspage-statusdetails></upl-statuspage-statusdetails> `;
public static demo = demoFunc;
@property({ type: Array })
public historyData: IStatusHistoryPoint[] = [];
@property({ type: String })
public serviceId: string = '';
@property({ type: String })
public serviceName: string = 'Service';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Number })
public hoursToShow: number = 48;
constructor() {
super();
@@ -27,69 +46,342 @@ export class UplStatuspageStatusdetails extends DeesElement {
public static styles = [
plugins.domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
position: relative;
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.mainbox {
margin: auto;
max-width: 900px;
text-align: right;
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
line-height: 50px;
border-radius: 3px;
background: ${colors.background.card};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.md)};
padding: ${unsafeCSS(spacing.md)};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.mainbox .barContainer {
position: relative;
display: flex;
padding: 6px;
gap: 2px;
padding: ${unsafeCSS(spacing.sm)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
overflow: hidden;
}
.mainbox .barContainer .bar {
margin: 4px;
width: 11px;
border-radius: 3px;
height: 40px;
background: #2deb51;
flex: 1;
height: 48px;
border-radius: ${unsafeCSS(borderRadius.sm)};
cursor: pointer;
transition: all 0.2s ease;
position: relative;
opacity: 0.9;
}
.mainbox .barContainer .bar:hover {
opacity: 1;
transform: scaleY(1.05);
}
.mainbox .barContainer .bar.operational {
background: ${colors.status.operational};
}
.mainbox .barContainer .bar.degraded {
background: ${colors.status.degraded};
}
.mainbox .barContainer .bar.partial_outage {
background: ${colors.status.partial};
}
.mainbox .barContainer .bar.major_outage {
background: ${colors.status.major};
}
.mainbox .barContainer .bar.maintenance {
background: ${colors.status.maintenance};
}
.mainbox .barContainer .bar.no-data {
background: ${colors.border.muted};
opacity: 0.3;
}
.timeIndicator {
position: absolute;
width: 11px;
height: 11px;
background: #FF9800;
top: 56px;
left: 400px;
transform: rotate(45deg);
width: 2px;
height: calc(100% - ${unsafeCSS(spacing.md)});
background: ${colors.text.primary};
top: ${unsafeCSS(spacing.sm)};
transition: left 0.3s;
opacity: 0.5;
pointer-events: none;
}
.time-labels {
display: flex;
justify-content: space-between;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.sm)} 0;
font-size: 12px;
color: ${colors.text.secondary};
font-family: ${unsafeCSS(fonts.mono)};
}
.tooltip {
position: absolute;
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 13px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
white-space: nowrap;
box-shadow: ${unsafeCSS(shadows.lg)};
border: 1px solid ${colors.border.default};
}
.tooltip.visible {
opacity: 0.95;
}
.tooltip strong {
font-weight: 600;
display: block;
margin-bottom: ${unsafeCSS(spacing.xs)};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
}
.tooltip div {
line-height: 1.4;
}
.loading-skeleton {
display: flex;
padding: ${unsafeCSS(spacing.sm)};
gap: 2px;
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
}
.loading-skeleton .skeleton-bar {
flex: 1;
height: 48px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.sm)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.status-legend {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
justify-content: center;
margin-top: ${unsafeCSS(spacing.lg)};
font-size: 13px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
}
.legend-color {
width: 12px;
height: 12px;
border-radius: ${unsafeCSS(borderRadius.sm)};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.mainbox {
padding: ${unsafeCSS(spacing.sm)};
}
.time-labels {
font-size: 11px;
}
.status-legend {
gap: ${unsafeCSS(spacing.md)};
font-size: 12px;
}
}
`,
];
public render(): TemplateResult {
const now = Date.now();
const currentHour = new Date().getHours();
const timeIndicatorPosition = this.calculateTimeIndicatorPosition();
return html`
<style></style>
<uplinternal-miniheading>Yesterday & Today</uplinternal-miniheading>
<uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
<div class="mainbox">
<div class="barContainer">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 48) {
counter++;
returnArray.push(html` <div class="bar"></div> `);
}
return returnArray;
})()}
<div class="timeIndicator"></div>
${this.loading ? html`
<div class="loading-skeleton">
${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
</div>
` : html`
<div class="barContainer" @mouseleave=${this.hideTooltip}>
${this.renderBars()}
<div class="timeIndicator" style="left: ${timeIndicatorPosition}px"></div>
</div>
<div class="time-labels">
<span>${this.getTimeLabel(0)}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 4))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 2))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow * 3 / 4))}</span>
<span>Now</span>
</div>
`}
</div>
${!this.loading ? html`
<div class="status-legend">
<div class="legend-item">
<div class="legend-color" style="background: #2deb51"></div>
<span>Operational</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #FF9800"></div>
<span>Degraded</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #FF6F00"></div>
<span>Partial Outage</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #F44336"></div>
<span>Major Outage</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #2196F3"></div>
<span>Maintenance</span>
</div>
</div>
` : ''}
<div class="tooltip" id="tooltip"></div>
`;
}
private renderBars(): TemplateResult[] {
const bars: TemplateResult[] = [];
const now = Date.now();
for (let i = 0; i < this.hoursToShow; i++) {
const hourIndex = this.hoursToShow - 1 - i;
const timestamp = now - (hourIndex * 60 * 60 * 1000);
const dataPoint = this.findDataPointForTime(timestamp);
const status = dataPoint?.status || 'no-data';
const responseTime = dataPoint?.responseTime || 0;
bars.push(html`
<div
class="bar ${status}"
@mouseenter=${(e: MouseEvent) => this.showTooltip(e, timestamp, status, responseTime)}
@click=${() => this.handleBarClick(timestamp, status, responseTime)}
></div>
`);
}
return bars;
}
private findDataPointForTime(timestamp: number): IStatusHistoryPoint | undefined {
if (!this.historyData || this.historyData.length === 0) return undefined;
// Find the closest data point within the same hour
const targetHour = new Date(timestamp).getHours();
const targetDate = new Date(timestamp).toDateString();
return this.historyData.find(point => {
const pointDate = new Date(point.timestamp);
return pointDate.toDateString() === targetDate &&
pointDate.getHours() === targetHour;
});
}
private calculateTimeIndicatorPosition(): number {
const containerWidth = 888; // Approximate width minus padding
const barWidth = 19; // Width + margin
const totalBars = this.hoursToShow;
const currentMinutes = new Date().getMinutes();
const currentPosition = (totalBars - 1 + currentMinutes / 60) * barWidth + 6;
return Math.min(currentPosition, containerWidth - 11);
}
private getTimeLabel(hoursAgo: number): string {
const date = new Date(Date.now() - (hoursAgo * 60 * 60 * 1000));
if (hoursAgo >= 24) {
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:00`;
}
return `${date.getHours()}:00`;
}
private showTooltip(event: MouseEvent, timestamp: number, status: string, responseTime: number) {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (!tooltip) return;
const date = new Date(timestamp);
const timeStr = date.toLocaleString();
const statusStr = status.replace(/_/g, ' ').replace('no-data', 'No Data');
tooltip.innerHTML = `
<div><strong>${timeStr}</strong></div>
<div>Status: ${statusStr}</div>
${responseTime > 0 ? `<div>Response Time: ${responseTime.toFixed(0)}ms</div>` : ''}
`;
const rect = (event.target as HTMLElement).getBoundingClientRect();
const containerRect = this.getBoundingClientRect();
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
tooltip.style.top = `${rect.top - containerRect.top - 60}px`;
tooltip.style.transform = 'translateX(-50%)';
tooltip.classList.add('visible');
}
private hideTooltip() {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (tooltip) {
tooltip.classList.remove('visible');
}
}
private handleBarClick(timestamp: number, status: string, responseTime: number) {
this.dispatchEvent(new CustomEvent('barClick', {
detail: { timestamp, status, responseTime, serviceId: this.serviceId },
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,876 @@
import { html } from '@design.estate/dees-element';
import type { IMonthlyUptime, IUptimeDay } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.stats-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-card {
background: white;
padding: 12px;
border-radius: 4px;
text-align: center;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #2196F3;
}
.stat-label {
font-size: 11px;
color: #666;
margin-top: 4px;
}
.month-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
</style>
<div class="demo-container">
<!-- Different Month Patterns -->
<div class="demo-section">
<div class="demo-title">Different Month Patterns</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Pattern generators
const generateMonthPattern = (monthCount: number, pattern: 'perfect' | 'problematic' | 'improving' | 'degrading' | 'seasonal'): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let monthOffset = monthCount - 1; monthOffset >= 0; monthOffset--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
for (let day = 1; day <= daysInMonth; day++) {
let uptime = 100;
let incidents = 0;
let downtime = 0;
let status: IUptimeDay['status'] = 'operational';
switch (pattern) {
case 'perfect':
// Near perfect uptime
if (Math.random() < 0.02) {
uptime = 99.9 + Math.random() * 0.099;
status = 'degraded';
}
break;
case 'problematic':
// Frequent issues
const problemRand = Math.random();
if (problemRand < 0.1) {
uptime = 70 + Math.random() * 20;
incidents = 2 + Math.floor(Math.random() * 3);
status = 'major_outage';
} else if (problemRand < 0.25) {
uptime = 90 + Math.random() * 8;
incidents = 1 + Math.floor(Math.random() * 2);
status = 'partial_outage';
} else if (problemRand < 0.4) {
uptime = 98 + Math.random() * 1.5;
incidents = 1;
status = 'degraded';
}
break;
case 'improving':
// Getting better over time
const improvementFactor = (monthCount - monthOffset) / monthCount;
const improveRand = Math.random();
if (improveRand < 0.3 * (1 - improvementFactor)) {
uptime = 85 + Math.random() * 10 + (improvementFactor * 10);
incidents = Math.max(0, 3 - Math.floor(improvementFactor * 3));
status = improvementFactor > 0.7 ? 'degraded' : 'partial_outage';
}
break;
case 'degrading':
// Getting worse over time
const degradationFactor = monthOffset / monthCount;
const degradeRand = Math.random();
if (degradeRand < 0.3 * (1 - degradationFactor)) {
uptime = 85 + Math.random() * 10 + (degradationFactor * 10);
incidents = Math.max(0, 3 - Math.floor(degradationFactor * 3));
status = degradationFactor > 0.7 ? 'degraded' : 'major_outage';
}
break;
case 'seasonal':
// Worse during certain months (simulating high traffic periods)
const monthNum = month;
if (monthNum === 11 || monthNum === 0) { // December, January
if (Math.random() < 0.3) {
uptime = 92 + Math.random() * 6;
incidents = 1 + Math.floor(Math.random() * 2);
status = 'degraded';
}
} else if (monthNum === 6 || monthNum === 7) { // July, August
if (Math.random() < 0.2) {
uptime = 94 + Math.random() * 5;
incidents = 1;
status = 'degraded';
}
}
break;
}
downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
const overallUptime = totalUptimeMinutes / (daysInMonth * 1440) * 100;
months.push({
month: monthKey,
days,
overallUptime,
totalIncidents
});
}
return months;
};
// Initial setup
statusMonth.serviceId = 'production-api';
statusMonth.serviceName = 'Production API';
statusMonth.monthlyData = generateMonthPattern(6, 'perfect');
// Create pattern controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const patterns = [
{ key: 'perfect', label: 'Perfect Uptime' },
{ key: 'problematic', label: 'Problematic' },
{ key: 'improving', label: 'Improving Trend' },
{ key: 'degrading', label: 'Degrading Trend' },
{ key: 'seasonal', label: 'Seasonal Pattern' }
];
patterns.forEach((pattern, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = pattern.label;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = generateMonthPattern(6, pattern.key as any);
statusMonth.loading = false;
updateStats();
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add statistics display
const statsDiv = document.createElement('div');
statsDiv.className = 'stats-display';
wrapperElement.appendChild(statsDiv);
const updateStats = () => {
const data = statusMonth.monthlyData || [];
const avgUptime = data.reduce((sum, month) => sum + month.overallUptime, 0) / data.length;
const totalIncidents = data.reduce((sum, month) => sum + month.totalIncidents, 0);
const worstMonth = data.reduce((worst, month) =>
month.overallUptime < worst.overallUptime ? month : worst, data[0]);
statsDiv.innerHTML = `
<div class="stat-card">
<div class="stat-value">${avgUptime.toFixed(3)}%</div>
<div class="stat-label">Avg Uptime</div>
</div>
<div class="stat-card">
<div class="stat-value">${totalIncidents}</div>
<div class="stat-label">Total Incidents</div>
</div>
<div class="stat-card">
<div class="stat-value">${data.length}</div>
<div class="stat-label">Months</div>
</div>
<div class="stat-card">
<div class="stat-value">${worstMonth ? worstMonth.overallUptime.toFixed(2) : '100'}%</div>
<div class="stat-label">Worst Month</div>
</div>
`;
};
updateStats();
// Handle day clicks
statusMonth.addEventListener('dayClick', (event: CustomEvent) => {
const { date, uptime, incidents, status, totalDowntime } = event.detail;
alert(`Day Details for ${date}:\n\nUptime: ${uptime.toFixed(3)}%\nIncidents: ${incidents}\nStatus: ${status}\nDowntime: ${totalDowntime} minutes`);
});
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Different Time Spans -->
<div class="demo-section">
<div class="demo-title">Different Time Spans</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Generate data for different time spans
const generateTimeSpanData = (months: number): IMonthlyUptime[] => {
const data: IMonthlyUptime[] = [];
const now = new Date();
for (let monthOffset = months - 1; monthOffset >= 0; monthOffset--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
for (let day = 1; day <= daysInMonth; day++) {
// Create realistic patterns
let uptime = 99.9 + Math.random() * 0.099;
let incidents = 0;
let status: IUptimeDay['status'] = 'operational';
if (Math.random() < 0.05) {
uptime = 95 + Math.random() * 4.9;
incidents = 1;
status = 'degraded';
} else if (Math.random() < 0.01) {
uptime = 85 + Math.random() * 10;
incidents = 2;
status = 'partial_outage';
}
const downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
const overallUptime = totalUptimeMinutes / (daysInMonth * 1440) * 100;
data.push({
month: monthKey,
days,
overallUptime,
totalIncidents
});
}
return data;
};
// Initial setup
statusMonth.serviceId = 'multi-region-lb';
statusMonth.serviceName = 'Multi-Region Load Balancer';
statusMonth.monthlyData = generateTimeSpanData(3);
// Create time span controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const timeSpans = [
{ months: 3, label: 'Last 3 Months' },
{ months: 6, label: 'Last 6 Months' },
{ months: 12, label: 'Last 12 Months' },
{ months: 24, label: 'Last 24 Months' }
];
timeSpans.forEach((span, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = span.label;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = generateTimeSpanData(span.months);
statusMonth.loading = false;
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Click on different time spans to see historical uptime data. The component automatically adjusts the display based on the number of months.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Current Month Real-time Updates -->
<div class="demo-section">
<div class="demo-title">Current Month with Real-time Updates</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Generate current month data
const generateCurrentMonthData = (): IMonthlyUptime[] => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const today = now.getDate();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
// Generate data only up to today
for (let day = 1; day <= today; day++) {
let uptime = 99.9 + Math.random() * 0.099;
let incidents = 0;
let status: IUptimeDay['status'] = 'operational';
// Today might have ongoing issues
if (day === today) {
if (Math.random() < 0.3) {
uptime = 95 + Math.random() * 4;
incidents = 1;
status = 'degraded';
}
} else if (Math.random() < 0.05) {
uptime = 97 + Math.random() * 2.9;
incidents = 1;
status = 'degraded';
}
const downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
// Fill remaining days with placeholder
for (let day = today + 1; day <= daysInMonth; day++) {
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime: 0,
incidents: 0,
totalDowntime: 0,
status: 'operational'
});
}
const overallUptime = today > 0 ? totalUptimeMinutes / (today * 1440) * 100 : 100;
return [{
month: monthKey,
days,
overallUptime,
totalIncidents
}];
};
// Initial setup
statusMonth.serviceId = 'realtime-monitor';
statusMonth.serviceName = 'Real-time Monitoring Service';
statusMonth.monthlyData = generateCurrentMonthData();
statusMonth.showCurrentDay = true;
// Update today's status periodically
const updateInterval = setInterval(() => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const currentMonth = data[0];
const today = new Date().getDate() - 1;
if (currentMonth.days[today]) {
// Simulate status changes
const rand = Math.random();
if (rand < 0.1) {
currentMonth.days[today].uptime = 95 + Math.random() * 4.9;
currentMonth.days[today].incidents = (currentMonth.days[today].incidents || 0) + 1;
currentMonth.days[today].status = 'degraded';
currentMonth.days[today].totalDowntime = Math.floor((100 - currentMonth.days[today].uptime) * 14.4);
// Recalculate overall uptime
let totalUptime = 0;
let validDays = 0;
currentMonth.days.forEach((day, index) => {
if (index <= today && day.uptime > 0) {
totalUptime += day.uptime;
validDays++;
}
});
currentMonth.overallUptime = validDays > 0 ? totalUptime / validDays : 100;
currentMonth.totalIncidents = currentMonth.days.reduce((sum, day) => sum + (day.incidents || 0), 0);
statusMonth.requestUpdate();
logUpdate('Status degraded - Uptime: ' + currentMonth.days[today].uptime.toFixed(2) + '%');
} else if (rand < 0.05 && currentMonth.days[today].status !== 'operational') {
// Recover from issues
currentMonth.days[today].uptime = 99.9 + Math.random() * 0.099;
currentMonth.days[today].status = 'operational';
currentMonth.days[today].totalDowntime = Math.floor((100 - currentMonth.days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Service recovered to operational status');
}
}
}
}, 3000);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = '<button class="demo-button" id="simulateOutage">Simulate Outage</button>' +
'<button class="demo-button" id="simulateRecovery">Simulate Recovery</button>' +
'<button class="demo-button" id="refreshData">Refresh Data</button>';
wrapperElement.appendChild(controls);
controls.querySelector('#simulateOutage')?.addEventListener('click', () => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const today = new Date().getDate() - 1;
data[0].days[today].uptime = 85 + Math.random() * 10;
data[0].days[today].incidents = (data[0].days[today].incidents || 0) + 1;
data[0].days[today].status = 'major_outage';
data[0].days[today].totalDowntime = Math.floor((100 - data[0].days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Major outage simulated');
}
});
controls.querySelector('#simulateRecovery')?.addEventListener('click', () => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const today = new Date().getDate() - 1;
data[0].days[today].uptime = 99.95;
data[0].days[today].status = 'operational';
data[0].days[today].totalDowntime = Math.floor((100 - data[0].days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Service recovered');
}
});
controls.querySelector('#refreshData')?.addEventListener('click', () => {
statusMonth.monthlyData = generateCurrentMonthData();
logUpdate('Data refreshed');
});
// Add update log
const logDiv = document.createElement('div');
logDiv.className = 'demo-info';
logDiv.style.maxHeight = '100px';
logDiv.style.overflowY = 'auto';
logDiv.innerHTML = '<strong>Update Log:</strong><br>';
wrapperElement.appendChild(logDiv);
const logUpdate = (message: string) => {
const time = new Date().toLocaleTimeString();
logDiv.innerHTML += '[' + time + '] ' + message + '<br>';
logDiv.scrollTop = logDiv.scrollHeight;
};
// Cleanup
wrapperElement.addEventListener('remove', () => {
clearInterval(updateInterval);
});
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const scenarios = {
noData: {
name: 'No Data',
data: []
},
singleMonth: {
name: 'Single Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => ({
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 99.9 + Math.random() * 0.099,
incidents: 0,
totalDowntime: 0,
status: 'operational' as const
})),
overallUptime: 99.95,
totalIncidents: 0
}]
},
allDown: {
name: 'Complete Outage Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => ({
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 0,
incidents: 5,
totalDowntime: 1440,
status: 'major_outage' as const
})),
overallUptime: 0,
totalIncidents: 155
}]
},
maintenanceMonth: {
name: 'Maintenance Heavy Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => {
// Maintenance every weekend
const dayOfWeek = new Date(2024, 0, i + 1).getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
return {
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 95,
incidents: 0,
totalDowntime: 72,
status: 'maintenance' as const
};
}
return {
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 99.95,
incidents: 0,
totalDowntime: 0.7,
status: 'operational' as const
};
}),
overallUptime: 98.2,
totalIncidents: 0
}]
},
mixedYear: {
name: 'Full Year Mixed',
data: Array.from({ length: 12 }, (_, monthIndex) => {
const year = 2023;
const month = monthIndex;
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Different pattern each quarter
let monthPattern = 'operational';
if (monthIndex < 3) monthPattern = 'degraded';
else if (monthIndex < 6) monthPattern = 'improving';
else if (monthIndex < 9) monthPattern = 'stable';
else monthPattern = 'volatile';
const days = Array.from({ length: daysInMonth }, (_, dayIndex) => {
let uptime = 99.9;
let status: IUptimeDay['status'] = 'operational';
let incidents = 0;
if (monthPattern === 'degraded' && Math.random() < 0.3) {
uptime = 85 + Math.random() * 10;
status = 'degraded';
incidents = 1;
} else if (monthPattern === 'volatile' && Math.random() < 0.2) {
uptime = 90 + Math.random() * 9;
status = Math.random() < 0.5 ? 'partial_outage' : 'degraded';
incidents = Math.floor(Math.random() * 3) + 1;
}
return {
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(dayIndex + 1).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: Math.floor((100 - uptime) * 14.4),
status
};
});
const totalIncidents = days.reduce((sum, day) => sum + day.incidents, 0);
const overallUptime = days.reduce((sum, day) => sum + day.uptime, 0) / days.length;
return {
month: `${year}-${String(month + 1).padStart(2, '0')}`,
days,
overallUptime,
totalIncidents
};
})
}
};
// Initial setup
let currentScenario = 'singleMonth';
statusMonth.serviceId = 'edge-case-service';
statusMonth.serviceName = 'Edge Case Service';
statusMonth.monthlyData = scenarios[currentScenario].data;
// Create scenario controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(scenarios).forEach(([key, scenario]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentScenario ? ' active' : '');
button.textContent = scenario.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentScenario = key;
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = scenario.data;
statusMonth.loading = false;
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Loading and Navigation States -->
<div class="demo-section">
<div class="demo-title">Loading and Navigation Features</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Start with loading
statusMonth.loading = true;
statusMonth.serviceId = 'navigation-demo';
statusMonth.serviceName = 'Navigation Demo Service';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = '<button class="demo-button" id="toggleLoading">Toggle Loading</button>' +
'<button class="demo-button" id="loadSuccess">Load Successfully</button>' +
'<button class="demo-button" id="loadError">Simulate Error</button>' +
'<button class="demo-button" id="toggleTooltip">Toggle Tooltip</button>';
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusMonth.loading = !statusMonth.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
statusMonth.loading = true;
setTimeout(() => {
const months = 6;
const data: IMonthlyUptime[] = [];
const now = new Date();
for (let i = months - 1; i >= 0; i--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
data.push({
month: `${year}-${String(month + 1).padStart(2, '0')}`,
days: Array.from({ length: daysInMonth }, (_, d) => ({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(d + 1).padStart(2, '0')}`,
uptime: 99 + Math.random(),
incidents: Math.random() < 0.05 ? 1 : 0,
totalDowntime: Math.random() < 0.05 ? Math.floor(Math.random() * 60) : 0,
status: Math.random() < 0.05 ? 'degraded' : 'operational'
})),
overallUptime: 99.5 + Math.random() * 0.4,
totalIncidents: Math.floor(Math.random() * 5)
});
}
statusMonth.monthlyData = data;
statusMonth.loading = false;
}, 1000);
});
controls.querySelector('#loadError')?.addEventListener('click', () => {
statusMonth.loading = true;
setTimeout(() => {
statusMonth.loading = false;
statusMonth.monthlyData = [];
statusMonth.errorMessage = 'Failed to load monthly uptime data';
}, 1500);
});
controls.querySelector('#toggleTooltip')?.addEventListener('click', () => {
statusMonth.showTooltip = !statusMonth.showTooltip;
const btn = controls.querySelector('#toggleTooltip');
if (btn) btn.textContent = 'Toggle Tooltip (' + (statusMonth.showTooltip ? 'ON' : 'OFF') + ')';
});
// Month navigation
const navDiv = document.createElement('div');
navDiv.className = 'month-nav';
navDiv.innerHTML = '<button class="demo-button" id="prevMonth">← Previous</button>' +
'<span id="currentMonth">Loading...</span>' +
'<button class="demo-button" id="nextMonth">Next →</button>';
wrapperElement.appendChild(navDiv);
let currentMonthIndex = 0;
const updateNavigation = () => {
const data = statusMonth.monthlyData || [];
if (data.length > 0 && currentMonthIndex < data.length) {
const month = data[currentMonthIndex];
const currentMonthEl = navDiv.querySelector('#currentMonth');
if (currentMonthEl) currentMonthEl.textContent = month.month;
const prevBtn = navDiv.querySelector('#prevMonth') as HTMLButtonElement;
const nextBtn = navDiv.querySelector('#nextMonth') as HTMLButtonElement;
if (prevBtn) prevBtn.disabled = currentMonthIndex === 0;
if (nextBtn) nextBtn.disabled = currentMonthIndex === data.length - 1;
}
};
navDiv.querySelector('#prevMonth')?.addEventListener('click', () => {
if (currentMonthIndex > 0) {
currentMonthIndex--;
updateNavigation();
// Highlight the month somehow
statusMonth.highlightMonth = statusMonth.monthlyData[currentMonthIndex].month;
}
});
navDiv.querySelector('#nextMonth')?.addEventListener('click', () => {
if (currentMonthIndex < (statusMonth.monthlyData?.length || 0) - 1) {
currentMonthIndex++;
updateNavigation();
statusMonth.highlightMonth = statusMonth.monthlyData[currentMonthIndex].month;
}
});
// Initial load
setTimeout(() => {
const data = Array.from({ length: 3 }, (_, i) => ({
month: `2024-${String(i + 1).padStart(2, '0')}`,
days: Array.from({ length: 31 }, (_, d) => ({
date: `2024-${String(i + 1).padStart(2, '0')}-${String(d + 1).padStart(2, '0')}`,
uptime: 99.5 + Math.random() * 0.5,
incidents: 0,
totalDowntime: 0,
status: 'operational' as const
})),
overallUptime: 99.7 + Math.random() * 0.3,
totalIncidents: Math.floor(Math.random() * 3)
}));
statusMonth.monthlyData = data;
statusMonth.loading = false;
statusMonth.showTooltip = true;
updateNavigation();
}, 1000);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -5,11 +5,15 @@ import {
customElement,
type TemplateResult,
css,
cssManager
cssManager,
unsafeCSS
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IMonthlyUptime } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusmonth.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,7 +23,25 @@ declare global {
@customElement('upl-statuspage-statusmonth')
export class UplStatuspageStatusmonth extends DeesElement {
public static demo = () => html` <upl-statuspage-statusmonth></upl-statuspage-statusmonth> `;
public static demo = demoFunc;
@property({ type: Array })
public monthlyData: IMonthlyUptime[] = [];
@property({ type: String })
public serviceId: string = '';
@property({ type: String })
public serviceName: string = 'Service';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Boolean })
public showTooltip: boolean = true;
@property({ type: Number })
public monthsToShow: number = 5;
constructor() {
super();
@@ -27,104 +49,310 @@ export class UplStatuspageStatusmonth extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
position: relative;
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.mainbox {
margin: auto;
max-width: 900px;
display: grid;
grid-template-columns: repeat(5, calc(100% / 5 - 80px / 5));
grid-column-gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${unsafeCSS(spacing.lg)};
}
.statusMonth {
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
min-height: 20px;
display: grid;
padding: 10px;
grid-template-columns: repeat(6, auto);
grid-gap: 9px;
border-radius: 3px;
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
box-shadow: ${unsafeCSS(shadows.sm)};
position: relative;
}
.statusMonth .statusDay {
width: 16px;
height: 16px;
background: #2deb51;
border-radius: 3px;
.month-header {
font-size: 14px;
font-weight: 600;
margin-bottom: ${unsafeCSS(spacing.md)};
text-align: center;
color: ${colors.text.primary};
letter-spacing: 0.02em;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}
.weekday-label {
font-size: 12px;
text-align: center;
color: ${colors.text.secondary};
font-weight: 500;
height: 20px;
line-height: 20px;
margin-bottom: ${unsafeCSS(spacing.xs)};
}
.statusDay {
aspect-ratio: 1;
border-radius: ${unsafeCSS(borderRadius.sm)};
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.statusDay:hover {
transform: scale(1.15);
box-shadow: ${unsafeCSS(shadows.md)};
z-index: 1;
}
.statusDay.operational {
background: ${colors.status.operational};
}
.statusDay.degraded {
background: ${colors.status.degraded};
}
.statusDay.partial_outage {
background: ${colors.status.partial};
}
.statusDay.major_outage {
background: ${colors.status.major};
}
.statusDay.no-data {
background: ${colors.border.muted};
opacity: 0.3;
}
.statusDay.empty {
background: transparent;
cursor: default;
}
.statusDay.empty:hover {
transform: none;
box-shadow: none;
}
.overall-uptime {
text-align: center;
font-size: 13px;
margin-top: ${unsafeCSS(spacing.md)};
color: ${colors.text.secondary};
line-height: 1.4;
}
.loading-skeleton {
height: 280px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.tooltip {
position: absolute;
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
white-space: nowrap;
box-shadow: ${unsafeCSS(shadows.lg)};
border: 1px solid ${colors.border.default};
}
.tooltip.visible {
opacity: 0.95;
}
.tooltip strong {
font-weight: 600;
display: block;
margin-bottom: ${unsafeCSS(spacing.xs)};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
}
.no-data-message {
grid-column: 1 / -1;
text-align: center;
padding: ${unsafeCSS(spacing['2xl'])};
color: ${colors.text.secondary};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.mainbox {
grid-template-columns: 1fr;
gap: ${unsafeCSS(spacing.md)};
}
.statusMonth {
padding: ${unsafeCSS(spacing.md)};
}
}
`
]
public render(): TemplateResult {
const totalDays = this.monthlyData.reduce((sum, month) => sum + month.days.length, 0);
return html`
<style></style>
<uplinternal-miniheading>Last 150 days</uplinternal-miniheading>
<div class="container">
<uplinternal-miniheading>${this.serviceName} - Last ${totalDays} Days</uplinternal-miniheading>
<div class="mainbox">
${this.loading ? html`
${Array(this.monthsToShow).fill(0).map(() => html`
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
<div class="loading-skeleton"></div>
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
`)}
` : this.monthlyData.length === 0 ? html`
<div class="no-data-message">No uptime data available</div>
` : this.monthlyData.map(month => this.renderMonth(month))}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
${this.showTooltip ? html`<div class="tooltip" id="tooltip"></div>` : ''}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
`;
}
return returnArray;
})()}
private renderMonth(monthData: IMonthlyUptime): TemplateResult {
const monthDate = new Date(monthData.month + '-01');
const monthName = monthDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
const firstDayOfWeek = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1).getDay();
return html`
<div class="statusMonth" @mouseleave=${this.hideTooltip}>
<div class="month-header">${monthName}</div>
<div class="days-grid">
${this.renderWeekdayLabels()}
${this.renderEmptyDays(firstDayOfWeek)}
${monthData.days.map(day => this.renderDay(day))}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
<div class="overall-uptime">
${monthData.overallUptime.toFixed(2)}% uptime
${monthData.totalIncidents > 0 ? html`<br/>${monthData.totalIncidents} incidents` : ''}
</div>
</div>
`;
}
private renderWeekdayLabels(): TemplateResult[] {
const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
return weekdays.map(day => html`<div class="weekday-label">${day}</div>`);
}
private renderEmptyDays(count: number): TemplateResult[] {
return Array(count).fill(0).map(() => html`<div class="statusDay empty"></div>`);
}
private renderDay(day: any): TemplateResult {
const status = day.status || 'no-data';
const date = new Date(day.date);
const dayNumber = date.getDate();
return html`
<div
class="statusDay ${status}"
@mouseenter=${(e: MouseEvent) => this.showTooltip && this.showDayTooltip(e, day)}
@click=${() => this.handleDayClick(day)}
>
${status === 'major_outage' || status === 'partial_outage' ? html`
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 8px;
font-weight: bold;
color: white;
">${day.incidents}</div>
` : ''}
</div>
`;
}
private showDayTooltip(event: MouseEvent, day: any) {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (!tooltip) return;
const date = new Date(day.date);
const dateStr = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
let statusText = day.status.replace(/_/g, ' ');
statusText = statusText.charAt(0).toUpperCase() + statusText.slice(1);
tooltip.innerHTML = `
<div><strong>${dateStr}</strong></div>
<div>Status: ${statusText}</div>
<div>Uptime: ${day.uptime.toFixed(2)}%</div>
${day.incidents > 0 ? `<div>Incidents: ${day.incidents}</div>` : ''}
${day.totalDowntime > 0 ? `<div>Downtime: ${day.totalDowntime} minutes</div>` : ''}
`;
const rect = (event.target as HTMLElement).getBoundingClientRect();
const containerRect = this.getBoundingClientRect();
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
tooltip.style.top = `${rect.top - containerRect.top - 80}px`;
tooltip.style.transform = 'translateX(-50%)';
tooltip.classList.add('visible');
}
private hideTooltip() {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (tooltip) {
tooltip.classList.remove('visible');
}
}
private handleDayClick(day: any) {
this.dispatchEvent(new CustomEvent('dayClick', {
detail: {
date: day.date,
uptime: day.uptime,
incidents: day.incidents,
status: day.status,
serviceId: this.serviceId
},
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,95 @@
export interface IServiceStatus {
id: string;
name: string;
displayName: string;
description?: string;
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
lastChecked: number; // timestamp
uptime30d: number; // percentage
uptime90d: number; // percentage
responseTime: number; // milliseconds
category?: string;
dependencies?: string[];
selected?: boolean;
}
export interface IStatusHistoryPoint {
timestamp: number;
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
responseTime?: number;
errorRate?: number;
}
export interface IIncidentUpdate {
id: string;
timestamp: number;
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
message: string;
author?: string;
}
export interface IIncidentDetails {
id: string;
title: string;
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
severity: 'critical' | 'major' | 'minor' | 'maintenance';
affectedServices: string[];
startTime: number;
endTime?: number;
updates: IIncidentUpdate[];
impact: string;
rootCause?: string;
resolution?: string;
}
export interface IUptimeDay {
date: string; // YYYY-MM-DD
uptime: number; // percentage
incidents: number;
totalDowntime: number; // minutes
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage';
}
export interface IMonthlyUptime {
month: string; // YYYY-MM
days: IUptimeDay[];
overallUptime: number; // percentage
totalIncidents: number;
}
export interface IOverallStatus {
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
message: string;
lastUpdated: number;
affectedServices: number;
totalServices: number;
}
export interface IStatusPageConfig {
apiEndpoint?: string;
refreshInterval?: number; // milliseconds
timeZone?: string;
dateFormat?: string;
enableWebSocket?: boolean;
enableNotifications?: boolean;
theme?: 'light' | 'dark' | 'auto';
language?: string;
showHistoricalDays?: number;
whitelabel?: boolean;
companyName?: string;
companyLogo?: string;
supportEmail?: string;
statusPageUrl?: string;
legalUrl?: string;
}
export interface ISubscription {
email?: string;
phone?: string;
webhook?: string;
services: string[];
severityFilter: ('critical' | 'major' | 'minor' | 'maintenance')[];
}
// Re-export the incident interface from @uptime.link/interfaces if needed
// Note: The IIncident interface is imported in the incidents component directly from plugins

View File

@@ -0,0 +1,181 @@
import { css, cssManager, unsafeCSS } from '@design.estate/dees-element';
export const fonts = {
base: `'Geist Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif`,
mono: `'Geist Mono', ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace`
};
export const colors = {
// Background colors
background: {
primary: cssManager.bdTheme('#ffffff', '#0a0a0a'),
secondary: cssManager.bdTheme('#f9fafb', '#18181b'),
muted: cssManager.bdTheme('#f3f4f6', '#27272a'),
card: cssManager.bdTheme('#ffffff', '#18181b')
},
// Border colors
border: {
default: cssManager.bdTheme('#e5e7eb', '#27272a'),
muted: cssManager.bdTheme('#f3f4f6', '#3f3f46')
},
// Text colors
text: {
primary: cssManager.bdTheme('#0a0a0a', '#fafafa'),
secondary: cssManager.bdTheme('#6b7280', '#a1a1aa'),
muted: cssManager.bdTheme('#9ca3af', '#71717a')
},
// Status colors
status: {
operational: cssManager.bdTheme('#10b981', '#064e3b'),
degraded: cssManager.bdTheme('#f59e0b', '#78350f'),
partial: cssManager.bdTheme('#ef4444', '#7f1d1d'),
major: cssManager.bdTheme('#dc2626', '#450a0a'),
maintenance: cssManager.bdTheme('#3b82f6', '#1e3a8a')
}
};
export const shadows = {
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
base: '0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)',
md: '0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
};
export const borderRadius = {
sm: '4px',
base: '6px',
md: '8px',
lg: '12px',
xl: '16px',
full: '9999px'
};
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px'
};
export const commonStyles = css`
/* Button styles */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: ${unsafeCSS(fonts.base)};
font-size: 14px;
font-weight: 500;
height: 36px;
padding: 0 16px;
border-radius: ${unsafeCSS(borderRadius.base)};
border: 1px solid ${colors.border.default};
background: transparent;
color: ${colors.text.primary};
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
gap: 8px;
}
.button:hover {
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
transform: translateY(-1px);
}
.button:active {
transform: translateY(0);
}
.button.primary {
background: ${colors.text.primary};
color: ${colors.background.primary};
border-color: ${colors.text.primary};
}
.button.primary:hover {
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
}
/* Card styles */
.card {
background: ${colors.background.card};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.md)};
padding: ${unsafeCSS(spacing.lg)};
box-shadow: ${unsafeCSS(shadows.sm)};
}
/* Loading skeleton */
.skeleton {
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.base)};
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Container styles */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)};
}
/* Responsive utilities */
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)};
}
}
`;
export const getStatusColor = (status: string) => {
switch (status) {
case 'operational':
return colors.status.operational;
case 'degraded':
return colors.status.degraded;
case 'partial_outage':
return colors.status.partial;
case 'major_outage':
return colors.status.major;
case 'maintenance':
return colors.status.maintenance;
default:
return colors.text.secondary;
}
};
export const getStatusIcon = (status: string) => {
switch (status) {
case 'operational':
return '✓';
case 'degraded':
return '!';
case 'partial_outage':
return '⚠';
case 'major_outage':
return '✕';
case 'maintenance':
return '🔧';
default:
return '?';
}
};