24 Commits

Author SHA1 Message Date
883648579e v1.2.0 2025-12-23 19:16:17 +00:00
183a1d0658 feat(statuspage-ui): improve styling and animations across status page components 2025-12-23 19:16:17 +00:00
213323073f v1.1.0 2025-12-23 09:26:37 +00:00
ed9728dd4a feat(statuspage): refactor shared styles and modernize components for consistent theming, spacing and APIs 2025-12-23 09:26:37 +00:00
891eb04d11 update 2025-06-30 07:54:17 +00:00
f9604263e3 update footer 2025-06-30 07:24:15 +00:00
df4dd3f539 update 2025-06-30 00:17:43 +00:00
bff7ec6640 update 2025-06-29 23:58:51 +00:00
bef2cdf2ce update 2025-06-29 23:53:05 +00:00
a20d46f561 update 2025-06-29 23:43:17 +00:00
bcddd4dbee update 2025-06-29 23:33:28 +00:00
5fcf910eab update 2025-06-29 23:25:42 +00:00
0f0764564b update 2025-06-29 22:58:33 +00:00
0ad46c1ed5 update 2025-06-29 22:49:47 +00:00
f6bf598481 update 2025-06-29 22:46:45 +00:00
f2ed469dbd update 2025-06-29 22:46:29 +00:00
f7d3709dac update 2025-06-29 22:36:12 +00:00
313e736f29 update 2025-06-29 19:55:58 +00:00
d12147716d 1.0.74 2024-06-27 12:20:10 +02:00
363cf32325 fix(core): Updated font loading strategy in index.html for improved performance 2024-06-27 12:20:09 +02:00
7e704483d0 1.0.73 2024-06-26 20:30:22 +02:00
ee54e62eab fix(documentation): Update project description and keywords in package.json and npmextra.json. Refactored documentation in readme.md. 2024-06-26 20:30:22 +02:00
3eeb971188 1.0.72 2024-06-26 20:28:10 +02:00
d2243d2376 fix(core): Fixed incorrect import paths and updated configurations for package publication. 2024-06-26 20:28:09 +02:00
49 changed files with 20831 additions and 2688 deletions

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/

View File

@@ -1,132 +0,0 @@
# gitzone ci_default_private
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
cache:
paths:
- .npmci_cache/
key: '$CI_BUILD_STAGE'
stages:
- security
- test
- release
- metadata
before_script:
- pnpm install -g pnpm
- pnpm install -g @shipzone/npmci
- npmci npm prepare
# ====================
# security stage
# ====================
# ====================
# security stage
# ====================
auditProductionDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci command npm config set registry https://registry.npmjs.org
- npmci command pnpm audit --audit-level=high --prod
tags:
- lossless
- docker
auditDevDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci command npm config set registry https://registry.npmjs.org
- npmci command pnpm audit --audit-level=high --dev
tags:
- lossless
- docker
allow_failure: true
# ====================
# test stage
# ====================
testStable:
stage: test
script:
- npmci node install stable
- npmci npm install
- npmci npm test
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- lossless
- docker
- notpriv
testBuild:
stage: test
script:
- npmci node install stable
- npmci npm install
- npmci command npm run build
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- lossless
- docker
- notpriv
release:
stage: release
script:
- npmci node install stable
- npmci npm publish
only:
- tags
tags:
- lossless
- docker
- notpriv
# ====================
# metadata stage
# ====================
codequality:
stage: metadata
allow_failure: true
only:
- tags
script:
- npmci command npm install -g typescript
- npmci npm prepare
- npmci npm install
tags:
- lossless
- docker
- priv
trigger:
stage: metadata
script:
- npmci trigger
only:
- tags
tags:
- lossless
- docker
- notpriv
pages:
stage: metadata
script:
- npmci node install lts
- npmci command npm install -g @gitzone/tsdoc
- npmci npm install
- npmci command tsdoc
tags:
- lossless
- docker
- notpriv
only:
- tags
artifacts:
expire_in: 1 week
paths:
- public
allow_failure: true

78
changelog.md Normal file
View File

@@ -0,0 +1,78 @@
# Changelog
## 2025-12-23 - 1.2.0 - feat(statuspage-ui)
improve styling and animations across status page components
- Preload Geist Sans variable font in html/index.html to improve font rendering.
- Replace many cssManager.bdTheme usages with sharedStyles color tokens, durations and easings for consistent theming.
- Introduce animations, transitions and motion utilities (fadeIn, fadeInUp, card/pill fades, shimmer, pulse) for cards, pills, bars, tooltips and skeletons to enhance perceived performance.
- Enhance interactive states: hover/active/focus for buttons, social links, action buttons, and asset/incident cards; add subtle transforms, shadows and icon animations.
- Add status-specific visuals: status gradients, status glows, pulsing/animated status indicators and left accent bars for statusbar and stat cards.
- Improve incidents and timeline UI: staggered entrance animations, active-incident pulse, update timeline with icons and delays; tooltip and tooltip content improvements; responsive tweaks across header, footer, stats and month views.
## 2025-12-23 - 1.1.0 - feat(statuspage)
refactor shared styles and modernize components for consistent theming, spacing and APIs
- Centralized styles: switch to a single sharedStyles module and extend design tokens (colors, spacing, shadows, borderRadius, easings, durations, accent colors).
- Component modernization: replace many destructured style imports with sharedStyles and convert numerous @property fields to 'accessor' for updated component APIs.
- Visual/layout updates: adjust typography, spacing scale, responsive rules and polish header/statusbar/footer/stats/incident components (improved gaps, paddings, status pills, loading skeletons, and button styles).
- Dependency bumps: updated @design.estate and @git.zone packages as well as @push.rocks dev deps and @types/node.
- Build/config changes: tsconfig.json flags removed (experimentalDecorators, useDefineForClassFields) and npmextra.json updated with registry/release config and new package keys.
- Docs and demos: expanded README content and added demo/test files (test/test.ts, test-shadcn-spacing.html) to showcase spacing and layout changes.
## 2024-06-27 - 1.0.74 - fix(core)
Updated font loading strategy in index.html for improved performance
- Replaced multiple font loading links with a single assetbroker link.
- Removed redundant preconnect and stylesheet links for fonts.
## 2024-06-26 - 1.0.73 - fix(documentation)
Update project description and keywords in package.json and npmextra.json. Refactored documentation in readme.md.
- Updated project description and keywords within package.json and npmextra.json to ensure alignment.
- Enhanced readme.md with detailed setup, installation and usage instructions including examples for importing and using components.
## 2024-06-26 - 1.0.72 - fix(core)
Fixed incorrect import paths and updated configurations for package publication.
- Fixed import paths for various dependencies in TypeScript files.
- Updated npm package name and scope in package.json.
- Deleted .gitlab-ci.yml file.
- Adjusted tsconfig.json for ESModule compatibility.
- Adjusted npmextra.json for correct repository and license information.
- Updated documentation links in readme.md.
## 2023-01-05 - 1.0.69 to 1.0.71 - core
Multiple updates.
- fix(core): update
## 2022-03-24 - 1.0.67 to 1.0.69 - core
Multiple updates.
- fix(core): update
## 2021-09-27 - 1.0.60 to 1.0.66 - core
Multiple updates.
- fix(core): update
## 2021-09-24 - 1.0.59 to 1.0.60 - core
Fixes and updates.
- fix(core): update
## 2021-09-23 - 1.0.58 to 1.0.59 - core
Fixes and updates.
- fix(core): update
## 2020-11-29 - 1.0.57 to 1.0.58 - core
Fixes and updates.
- fix(core): update
## 2020-09-19 - 1.0.55 to 1.0.57 - core
Fixes and updates.
- fix(core): update

228
example.statuspage.html Normal file
View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Status Page Example</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
.page-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>
</head>
<body>
<div class="page-container">
<!-- Header -->
<upl-statuspage-header
title="System Status"
brandName="Example Corp"
description="Real-time status of our services"
></upl-statuspage-header>
<!-- Status Bar -->
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<!-- Stats Grid - NEW COMPONENT -->
<upl-statuspage-statsgrid
uptime="99.95"
avgResponseTime="125"
totalIncidents="0"
affectedServices="0"
totalServices="12"
currentStatus="operational"
timePeriod="90 days"
></upl-statuspage-statsgrid>
<main>
<!-- Assets Selector -->
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
<!-- Status Details (48 hours) -->
<upl-statuspage-statusdetails
serviceName="API Gateway"
hoursToShow="48"
></upl-statuspage-statusdetails>
<!-- Status Month -->
<upl-statuspage-statusmonth
serviceName="API Gateway"
monthsToShow="3"
></upl-statuspage-statusmonth>
<!-- Incidents -->
<upl-statuspage-incidents
daysToShow="90"
></upl-statuspage-incidents>
</main>
<!-- Footer -->
<upl-statuspage-footer
showPoweredBy="true"
showApiLink="true"
></upl-statuspage-footer>
</div>
<script>
// Example: Initialize with demo data
document.addEventListener('DOMContentLoaded', () => {
// Status bar data
const statusBar = document.querySelector('upl-statuspage-statusbar');
if (statusBar) {
statusBar.overallStatus = {
status: 'operational',
message: 'All systems operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 12
};
}
// Assets selector data
const assetsSelector = document.querySelector('upl-statuspage-assetsselector');
if (assetsSelector) {
assetsSelector.services = [
{
id: 'api-gateway',
displayName: 'API Gateway',
category: 'Core Services',
description: 'Main API endpoint',
currentStatus: 'operational',
selected: true
},
{
id: 'web-app',
displayName: 'Web Application',
category: 'Frontend',
description: 'Customer-facing web app',
currentStatus: 'operational',
selected: true
},
{
id: 'database',
displayName: 'Database Cluster',
category: 'Core Services',
description: 'Primary data storage',
currentStatus: 'operational',
selected: true
},
{
id: 'cdn',
displayName: 'CDN',
category: 'Infrastructure',
description: 'Content delivery network',
currentStatus: 'operational',
selected: false
}
];
}
// Status details data (48 hours)
const statusDetails = document.querySelector('upl-statuspage-statusdetails');
if (statusDetails) {
const now = Date.now();
statusDetails.historyData = Array.from({ length: 48 }, (_, i) => {
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - (47 - i));
return {
timestamp: date.getTime(),
status: Math.random() > 0.95 ? 'degraded' : 'operational',
responseTime: 50 + Math.random() * 50,
errorRate: 0
};
});
}
// Monthly data
const statusMonth = document.querySelector('upl-statuspage-statusmonth');
if (statusMonth) {
const generateMonthData = (monthOffset) => {
const date = new Date();
date.setMonth(date.getMonth() - monthOffset);
const year = date.getFullYear();
const month = date.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
return {
month: `${year}-${(month + 1).toString().padStart(2, '0')}`,
days: Array.from({ length: daysInMonth }, (_, i) => ({
date: `${year}-${(month + 1).toString().padStart(2, '0')}-${(i + 1).toString().padStart(2, '0')}`,
uptime: Math.random() > 0.05 ? 100 : 95 + Math.random() * 5,
status: Math.random() > 0.05 ? 'operational' : 'degraded',
incidents: Math.random() > 0.05 ? 0 : 1,
totalDowntime: 0
})),
overallUptime: 99.5 + Math.random() * 0.5,
totalIncidents: Math.floor(Math.random() * 3)
};
};
statusMonth.monthlyData = [
generateMonthData(0),
generateMonthData(1),
generateMonthData(2)
];
}
// Incidents data
const incidents = document.querySelector('upl-statuspage-incidents');
if (incidents) {
incidents.currentIncidents = [];
incidents.pastIncidents = [
{
id: 'inc-001',
title: 'Database connection pool exhaustion',
severity: 'minor',
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
impact: 'Slower response times for some API endpoints',
affectedServices: ['API Gateway', 'Database Cluster'],
updates: [
{
id: 'update-1',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'We are investigating reports of slow API responses',
author: 'Engineering Team'
},
{
id: 'update-2',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
status: 'identified',
message: 'The issue has been identified as database connection pool exhaustion',
author: 'Engineering Team'
},
{
id: 'update-3',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
status: 'resolved',
message: 'Connection pool settings have been adjusted and performance is back to normal',
author: 'Engineering Team'
}
],
rootCause: 'Insufficient connection pool size for peak traffic',
resolution: 'Increased connection pool size and implemented better connection management'
}
];
}
});
</script>
</body>
</html>

View File

@@ -10,16 +10,10 @@
/> />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="preconnect" href="https://rsms.me/"> <!--Lets load standard fonts-->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preload" href="https://assetbroker.lossless.one/fonts/geist-sans/geistvf.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> <link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
<link
crossorigin="anonymous"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<style> <style>
body { body {

View File

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

View File

@@ -1,19 +1,39 @@
{ {
"gitzone": { "@git.zone/cli": {
"projectType": "wcc", "projectType": "wcc",
"module": { "module": {
"githost": "gitlab.com", "githost": "code.foss.global",
"gitscope": "uptimelink/private", "gitscope": "uptime.link",
"gitrepo": "catalog", "gitrepo": "statuspage",
"description": "a catalog with webcomponents for uptimelink dashboard", "description": "A catalog of web components for the UptimeLink dashboard.",
"npmPackagename": "@uptimelink_private/catalog", "npmPackagename": "@uptime.link/statuspage",
"license": "UNLICENSED", "license": "MIT",
"projectDomain": "uptime.link" "projectDomain": "uptime.link",
"keywords": [
"web components",
"uptimelink",
"dashboard",
"status monitoring",
"typescript",
"incidents",
"status",
"performance",
"uptime",
"frontend",
"UI",
"catalog"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"npmAccessLevel": "private",
"npmRegistryUrl": "verdaccio.lossless.one" "npmRegistryUrl": "verdaccio.lossless.one"
} }
} }

View File

@@ -1,34 +1,33 @@
{ {
"name": "@uptimelink_private/catalog", "name": "@uptime.link/statuspage",
"version": "1.0.71", "version": "1.2.0",
"private": false, "private": false,
"description": "a catalog with webcomponents for uptimelink dashboard", "description": "A catalog of web components for the UptimeLink dashboard.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
"typings": "dist_ts_web/index.d.ts", "typings": "dist_ts_web/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "npm run build", "test": "npm run build",
"build": "tsbuild element --allowimplicitany && tsbundle element --production", "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
"watch": "tswatch element", "watch": "tswatch element",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@designestate/dees-domtools": "^2.0.1", "@design.estate/dees-domtools": "^2.3.6",
"@designestate/dees-element": "^2.0.4", "@design.estate/dees-element": "^2.1.3",
"@designestate/dees-wcctools": "^1.0.73", "@design.estate/dees-wcctools": "^3.2.0",
"@losslessone_private/loint-pubapi": "^1.0.10", "@uptime.link/interfaces": "^2.0.21"
"@uptimelink/interfaces": "^1.0.10"
}, },
"devDependencies": { "devDependencies": {
"@gitzone/tsbuild": "^2.1.61", "@git.zone/tsbuild": "^4.0.2",
"@gitzone/tsbundle": "^2.0.7", "@git.zone/tsbundle": "^2.6.3",
"@gitzone/tsrun": "^1.2.39", "@git.zone/tsrun": "^2.0.1",
"@gitzone/tswatch": "^2.0.5", "@git.zone/tswatch": "^2.3.13",
"@pushrocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.2",
"@pushrocks/smartenv": "^5.0.0", "@push.rocks/smartenv": "^6.0.0",
"@types/node": "^18.11.18" "@types/node": "^25.0.3"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -44,5 +43,19 @@
], ],
"browserslist": [ "browserslist": [
"last 1 Chrome versions" "last 1 Chrome versions"
],
"keywords": [
"web components",
"uptimelink",
"dashboard",
"status monitoring",
"typescript",
"incidents",
"status",
"performance",
"uptime",
"frontend",
"UI",
"catalog"
] ]
} }

8570
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

292
readme.hints.md Normal file
View File

@@ -0,0 +1,292 @@
# 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)
### Component Order (Top to Bottom)
1. `upl-statuspage-header` - Navigation header
2. `upl-statuspage-statusbar` - Overall system status
3. `upl-statuspage-statsgrid` - Key metrics grid (uptime, response time, incidents)
4. `upl-statuspage-assetsselector` - Service selection
5. `upl-statuspage-statusdetails` - 48-hour status visualization
6. `upl-statuspage-statusmonth` - Monthly calendar view
7. `upl-statuspage-incidents` - Current and past incidents
8. `upl-statuspage-footer` - Page footer
### 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
8. **upl-statuspage-statsgrid** (NEW)
- Displays key performance metrics in a responsive grid
- Shows current status with color indicator
- Uptime percentage with configurable time period
- Average response time with performance trend indicators
- Total incidents count with affected services
- Loading state with skeleton animation
- Responsive design that stacks on mobile
- Used stats data that was previously in statusdetails component
### 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
- IStatusHistoryPoint - 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.

402
readme.md
View File

@@ -1,32 +1,382 @@
# @uptimelink/private/catalog # @uptime.link/statuspage
a catalog with webcomponents for uptimelink dashboard
## Availabililty and Links 🚀 **A powerful collection of web components for building stunning status pages** — because your users deserve to know what's happening, and you deserve to look good while telling them.
* [npmjs.org (npm package)](https://www.npmjs.com/package/@uptimelink_private/catalog)
* [gitlab.com (source)](https://gitlab.com/uptimelink/private/catalog)
* [github.com (source mirror)](https://github.com/uptimelink/private/catalog)
* [docs (typedoc)](https://uptimelink/private.gitlab.io/catalog/)
## Status for master Built with [Lit](https://lit.dev/) and TypeScript, these components are designed to be composable, customizable, and production-ready out of the box.
Status Category | Status Badge ## Issue Reporting and Security
-- | --
GitLab Pipelines | [![pipeline status](https://gitlab.com/uptimelink/private/catalog/badges/master/pipeline.svg)](https://lossless.cloud)
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/uptimelink/private/catalog/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@uptimelink_private/catalog)](https://lossless.cloud)
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/uptimelink/private/catalog)](https://lossless.cloud)
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud)
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud)
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@uptimelink_private/catalog)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@uptimelink_private/catalog)](https://lossless.cloud)
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@uptimelink_private/catalog)](https://lossless.cloud)
## Usage For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
Use TypeScript for best in class intellisense ---
For further information read the linked docs at the top of this readme.
## Legal ## ✨ Features
> UNLICENSED licensed | **&copy;** [Task Venture Capital GmbH](https://task.vc)
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy) - 🎨 **Themeable** — Automatic light/dark mode support with CSS custom properties
- 📱 **Responsive** — Looks great on everything from mobile to ultrawide
- 🔌 **Composable** — Use components individually or build complete status pages
- 🎯 **Type-Safe** — Full TypeScript support with exported interfaces
-**Performant** — Lit-based components with minimal overhead
- 🎭 **Pre-built Pages** — Demo, all-green, outage, and maintenance page templates included
---
## 📦 Installation
```bash
npm install @uptime.link/statuspage
```
Or with pnpm:
```bash
pnpm add @uptime.link/statuspage
```
---
## 🚀 Quick Start
Import the components and start building:
```typescript
import '@uptime.link/statuspage';
```
Then use them in your HTML:
```html
<upl-statuspage-header></upl-statuspage-header>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<upl-statuspage-incidents></upl-statuspage-incidents>
<upl-statuspage-footer></upl-statuspage-footer>
```
---
## 🧩 Components
### `<upl-statuspage-header>`
The main navigation header with branding and action buttons.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `pageTitle` | `string` | `'Status'` | Title displayed in the header |
| `logoUrl` | `string` | `''` | URL to your logo image |
| `showReportButton` | `boolean` | `false` | Show "Report Incident" button |
| `showSubscribeButton` | `boolean` | `false` | Show "Subscribe" button |
**Events:**
- `reportNewIncident` — Fired when report button is clicked
- `statusSubscribe` — Fired when subscribe button is clicked
---
### `<upl-statuspage-pagetitle>`
A hero section with title and subtitle.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `pageTitle` | `string` | `'System Status'` | Main heading |
| `pageSubtitle` | `string` | `''` | Optional description text |
| `centered` | `boolean` | `false` | Center-align the content |
---
### `<upl-statuspage-statusbar>`
The overall status indicator — the heart of any status page.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `overallStatus` | `IOverallStatus` | — | Current system status object |
| `loading` | `boolean` | `false` | Show loading skeleton |
The status object supports: `operational`, `degraded`, `partial_outage`, `major_outage`, `maintenance`
---
### `<upl-statuspage-statsgrid>`
Key metrics at a glance — uptime, response time, incidents.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `currentStatus` | `string` | `'operational'` | Current status indicator |
| `uptime` | `number` | `100` | Uptime percentage |
| `avgResponseTime` | `number` | `0` | Average response time (ms) |
| `totalIncidents` | `number` | `0` | Incident count |
| `affectedServices` | `number` | `0` | Currently affected services |
| `totalServices` | `number` | `0` | Total monitored services |
| `timePeriod` | `string` | `'30 days'` | Time range label |
| `loading` | `boolean` | `false` | Show loading skeleton |
---
### `<upl-statuspage-assetsselector>`
Interactive service selector with filtering and search.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `services` | `IServiceStatus[]` | `[]` | Array of service objects |
| `filterText` | `string` | `''` | Search filter text |
| `filterCategory` | `string` | `'all'` | Category filter |
| `showOnlySelected` | `boolean` | `false` | Show only selected services |
| `loading` | `boolean` | `false` | Show loading state |
| `expanded` | `boolean` | `false` | Expand the selector panel |
**Events:**
- `selectionChanged` — Fired with `{ selectedServices: string[] }` when selection changes
---
### `<upl-statuspage-statusdetails>`
Hourly status timeline visualization.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `dataPoints` | `IStatusHistoryPoint[]` | `[]` | Hourly status data |
| `historyData` | `IStatusHistoryPoint[]` | `[]` | Alternative data property |
| `serviceId` | `string` | `''` | Service identifier |
| `serviceName` | `string` | `'Service'` | Display name |
| `hoursToShow` | `number` | `48` | Number of hours to display |
| `loading` | `boolean` | `false` | Show loading skeleton |
**Events:**
- `barClick` — Fired with `{ timestamp, status, responseTime, serviceId }` on bar click
---
### `<upl-statuspage-statusmonth>`
Calendar-style monthly uptime visualization.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `monthlyData` | `IMonthlyUptime[]` | `[]` | Monthly uptime data |
| `serviceId` | `string` | `'all'` | Service identifier |
| `serviceName` | `string` | `'All Services'` | Display name |
| `loading` | `boolean` | `false` | Show loading skeleton |
**Events:**
- `dayClick` — Fired with day details when a day cell is clicked
---
### `<upl-statuspage-incidents>`
Incident feed with expandable details and status updates.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `currentIncidents` | `IIncidentDetails[]` | `[]` | Active incidents |
| `pastIncidents` | `IIncidentDetails[]` | `[]` | Resolved incidents |
| `maxPastIncidents` | `number` | `10` | Max past incidents to show |
| `loading` | `boolean` | `false` | Show loading skeleton |
| `enableSubscription` | `boolean` | `false` | Allow incident subscriptions |
| `subscribedIncidentIds` | `string[]` | `[]` | Pre-subscribed incident IDs |
**Events:**
- `subscribeToIncident` — Fired with incident ID on subscription toggle
---
### `<upl-statuspage-footer>`
Full-featured footer with links, social icons, and subscription.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `companyName` | `string` | `''` | Company name |
| `legalUrl` | `string` | `''` | Terms/legal page URL |
| `supportEmail` | `string` | `''` | Support email address |
| `statusPageUrl` | `string` | `''` | Status page URL |
| `lastUpdated` | `number` | — | Last update timestamp |
| `currentYear` | `number` | `2024` | Copyright year |
| `socialLinks` | `ISocialLink[]` | `[]` | Social media links |
| `rssFeedUrl` | `string` | `''` | RSS feed URL |
| `apiStatusUrl` | `string` | `''` | Status API URL |
| `enableSubscribe` | `boolean` | `false` | Show subscribe button |
| `subscriberCount` | `number` | `0` | Display subscriber count |
| `additionalLinks` | `IFooterLink[]` | `[]` | Extra footer links |
**Events:**
- `subscribeClick` — Fired when subscribe button is clicked
---
## 📐 Interfaces
All TypeScript interfaces are exported for type safety:
```typescript
import type {
IServiceStatus,
IStatusHistoryPoint,
IIncidentDetails,
IIncidentUpdate,
IMonthlyUptime,
IUptimeDay,
IOverallStatus,
IStatusPageConfig,
ISubscription
} from '@uptime.link/statuspage';
```
### Key Interfaces
```typescript
interface IServiceStatus {
id: string;
name: string;
displayName: string;
description?: string;
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
lastChecked: number;
uptime30d: number;
uptime90d: number;
responseTime: number;
category?: string;
selected?: boolean;
}
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;
}
interface IOverallStatus {
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
message: string;
lastUpdated: number;
affectedServices: number;
totalServices: number;
}
```
---
## 🎬 Complete Example
Build a full status page in minutes:
```typescript
import '@uptime.link/statuspage';
import type { IServiceStatus, IOverallStatus, IIncidentDetails } from '@uptime.link/statuspage';
// Get your components
const header = document.querySelector('upl-statuspage-header');
const statusBar = document.querySelector('upl-statuspage-statusbar');
const statsGrid = document.querySelector('upl-statuspage-statsgrid');
const incidents = document.querySelector('upl-statuspage-incidents');
const footer = document.querySelector('upl-statuspage-footer');
// Configure the header
header.pageTitle = 'Acme Cloud';
header.logoUrl = '/logo.svg';
header.showSubscribeButton = true;
// Set overall status
statusBar.overallStatus = {
status: 'operational',
message: 'All systems operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 12
};
// Configure stats
statsGrid.uptime = 99.98;
statsGrid.avgResponseTime = 45;
statsGrid.totalServices = 12;
// Add incidents
incidents.currentIncidents = []; // No current incidents
incidents.pastIncidents = [
{
id: 'inc-001',
title: 'Scheduled Maintenance Complete',
status: 'resolved',
severity: 'maintenance',
impact: 'Database maintenance window',
affectedServices: ['Database'],
startTime: Date.now() - 86400000,
endTime: Date.now() - 82800000,
updates: [
{
id: 'upd-1',
timestamp: Date.now() - 82800000,
status: 'resolved',
message: 'Maintenance completed successfully'
}
]
}
];
// Configure footer
footer.companyName = 'Acme Cloud';
footer.supportEmail = 'support@acme.cloud';
footer.enableSubscribe = true;
```
---
## 📄 Pre-built Page Templates
The package includes ready-to-use page templates:
```typescript
import {
statuspageDemo, // Full demo with sample data
statuspageAllgreen, // All systems operational
statuspageOutage, // Major outage scenario
statuspageMaintenance // Scheduled maintenance
} from '@uptime.link/statuspage/pages';
```
---
## 🎨 Theming
Components automatically adapt to light/dark mode using `cssManager.bdTheme()`. The design follows a modern, minimal aesthetic with:
- Clean typography with `-apple-system, BlinkMacSystemFont, 'Segoe UI'` font stack
- Subtle shadows and borders
- Semantic status colors (green, yellow, orange, red, blue)
- Smooth transitions and hover states
---
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

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.

84
test-demo-no-banners.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo Pages Without Banners</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
.page-selector {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
@media (prefers-color-scheme: dark) {
.page-selector {
background: #18181b;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
}
</style>
</head>
<body>
<div class="page-selector">
<label for="demoSelect">Select Demo:</label>
<select id="demoSelect">
<option value="demo">Main Demo (Degraded)</option>
<option value="allgreen">All Green</option>
<option value="outage">Major Outage</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
<div id="demoContainer"></div>
<script type="module">
import { statuspageDemo } from './dist_ts_web/pages/statuspage-demo.js';
import { statuspageAllgreen } from './dist_ts_web/pages/statuspage-allgreen.js';
import { statuspageOutage } from './dist_ts_web/pages/statuspage-outage.js';
import { statuspageMaintenance } from './dist_ts_web/pages/statuspage-maintenance.js';
const demoContainer = document.getElementById('demoContainer');
const demoSelect = document.getElementById('demoSelect');
const demos = {
demo: statuspageDemo,
allgreen: statuspageAllgreen,
outage: statuspageOutage,
maintenance: statuspageMaintenance
};
function loadDemo(demoName) {
const demoFunc = demos[demoName];
if (demoFunc) {
demoContainer.innerHTML = '';
const template = demoFunc();
demoContainer.innerHTML = template.strings.join('');
}
}
demoSelect.addEventListener('change', (e) => {
loadDemo(e.target.value);
});
// Load initial demo
loadDemo('demo');
</script>
</body>
</html>

132
test-footer-shadcn.html Normal file
View File

@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Footer Component - shadcn Style</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
min-height: 100vh;
display: flex;
flex-direction: column;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
main {
flex: 1;
padding: 40px;
text-align: center;
color: #6b7280;
}
@media (prefers-color-scheme: dark) {
main {
color: #a1a1aa;
}
}
.demo-section {
margin-top: auto;
}
h1 {
margin-bottom: 20px;
color: #0a0a0a;
}
@media (prefers-color-scheme: dark) {
h1 {
color: #fafafa;
}
}
</style>
</head>
<body>
<main>
<h1>Footer Component with Full shadcn Styling</h1>
<p>The footer below has been fully updated to embrace shadcn design principles:</p>
<ul style="text-align: left; max-width: 600px; margin: 20px auto;">
<li>Minimal design with subtle borders</li>
<li>Consistent spacing and typography</li>
<li>No transform effects, only color transitions</li>
<li>Flat design with no gradients</li>
<li>Proper muted colors for secondary text</li>
<li>Clean hover states</li>
<li>Simplified social icons in square containers</li>
<li>Native-looking form controls</li>
</ul>
</main>
<div class="demo-section">
<upl-statuspage-footer
id="footer1"
company-name="Example Corp"
legal-url="https://example.com/legal"
support-email="support@example.com"
status-page-url="https://status.example.com"
show-powered-by="true"
enable-subscribe="true"
show-api-link="true"
rss-feed-url="https://status.example.com/rss"
api-status-url="https://api.example.com/status"
enable-language-selector="true"
enable-theme-toggle="true"
subscriber-count="1234"
></upl-statuspage-footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const footer = document.getElementById('footer1');
// Set social links
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/example' },
{ platform: 'github', url: 'https://github.com/example' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/example' },
{ platform: 'youtube', url: 'https://youtube.com/example' }
];
// Set additional links
footer.additionalLinks = [
{ label: 'API Documentation', url: 'https://docs.example.com' },
{ label: 'Privacy Policy', url: 'https://example.com/privacy' },
{ label: 'Terms of Service', url: 'https://example.com/terms' }
];
// Set supported languages
footer.supportedLanguages = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'fr', name: 'Français' },
{ code: 'es', name: 'Español' }
];
footer.currentLanguage = 'en';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
// Event listeners
footer.addEventListener('subscribeClick', () => {
alert('Subscribe clicked - would open subscription form');
});
footer.addEventListener('reportIssueClick', () => {
alert('Report issue clicked - would open issue form');
});
footer.addEventListener('languageChange', (e) => {
console.log('Language changed to:', e.detail.language);
});
footer.addEventListener('themeToggle', () => {
console.log('Theme toggle clicked');
});
});
</script>
</body>
</html>

105
test-footer-subscribe.html Normal file
View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Footer Subscribe Button Test</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
min-height: 100vh;
display: flex;
flex-direction: column;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
main {
flex: 1;
padding: 40px;
text-align: center;
}
h1 {
margin-bottom: 20px;
color: #0a0a0a;
}
@media (prefers-color-scheme: dark) {
h1 {
color: #fafafa;
}
}
.demo-section {
margin-top: auto;
}
.comparison {
display: flex;
gap: 40px;
justify-content: center;
margin: 40px 0;
}
.example {
text-align: center;
}
.example h3 {
margin-bottom: 20px;
color: #666;
}
@media (prefers-color-scheme: dark) {
.example h3 {
color: #a1a1aa;
}
}
</style>
</head>
<body>
<main>
<h1>Footer Subscribe Button - Fixed Layout</h1>
<p>The subscriber count is now displayed below the button instead of inside it:</p>
<div class="comparison">
<div class="example">
<h3>Without Subscribers</h3>
<p>Button appears alone when subscriber count is 0</p>
</div>
<div class="example">
<h3>With Subscribers</h3>
<p>Count appears below the button as secondary text</p>
</div>
</div>
</main>
<div class="demo-section">
<upl-statuspage-footer
id="footer1"
company-name="Example Corp"
enable-subscribe="true"
subscriber-count="1234"
></upl-statuspage-footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const footer = document.getElementById('footer1');
footer.addEventListener('subscribeClick', () => {
alert('Subscribe button clicked! The subscriber count is displayed separately below the button.');
});
// Demonstrate different subscriber counts
let counts = [0, 42, 1234, 50000];
let currentIndex = 0;
setInterval(() => {
currentIndex = (currentIndex + 1) % counts.length;
footer.subscriberCount = counts[currentIndex];
}, 3000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Incident Subscription Test</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 20px;
color: #0a0a0a;
}
@media (prefers-color-scheme: dark) {
h1 {
color: #fafafa;
}
}
.info-panel {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
@media (prefers-color-scheme: dark) {
.info-panel {
background: #0c4a6e;
border-color: #0284c7;
}
}
.subscription-log {
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
margin-top: 30px;
font-family: monospace;
font-size: 13px;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.subscription-log {
background: #1f2937;
border-color: #374151;
color: #e5e7eb;
}
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
background: #10b981;
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-size: 14px;
font-weight: 500;
animation: slideIn 0.3s ease;
z-index: 1000;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.unsubscribe {
background: #6b7280;
}
</style>
</head>
<body>
<div class="container">
<h1>Incident Subscription Feature Demo</h1>
<div class="info-panel">
<h3>How Incident Subscriptions Work:</h3>
<ul>
<li>Each incident can be individually subscribed to</li>
<li>Click on an incident to expand it and see the "Subscribe to updates" button</li>
<li>Subscribed incidents show a checkmark and "Subscribed to updates" text</li>
<li>Click again to unsubscribe</li>
<li>The component emits events for subscribe/unsubscribe actions</li>
<li>You can pre-set subscribed incidents using the <code>subscribedIncidentIds</code> property</li>
</ul>
</div>
<upl-statuspage-incidents id="incidents"></upl-statuspage-incidents>
<div class="subscription-log" id="log">
<strong>Event Log:</strong><br>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const incidents = document.getElementById('incidents');
const log = document.getElementById('log');
// Helper function to add log entries
function addLog(message) {
const timestamp = new Date().toLocaleTimeString();
log.innerHTML += `[${timestamp}] ${message}<br>`;
log.scrollTop = log.scrollHeight;
}
// Helper function to show notifications
function showNotification(message, type = 'subscribe') {
const notification = document.createElement('div');
notification.className = `notification ${type === 'unsubscribe' ? 'unsubscribe' : ''}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Set up some test incidents
const currentIncidents = [
{
id: 'inc-001',
title: 'Database Connection Pool Exhaustion',
status: 'monitoring',
severity: 'major',
affectedServices: ['API Gateway', 'User Service', 'Order Service'],
startTime: Date.now() - 2 * 60 * 60 * 1000,
impact: 'API response times are elevated. Some requests may timeout.',
updates: [
{
id: 'upd-1',
timestamp: Date.now() - 2 * 60 * 60 * 1000,
status: 'investigating',
message: 'We are investigating reports of slow API responses',
author: 'Operations Team'
},
{
id: 'upd-2',
timestamp: Date.now() - 90 * 60 * 1000,
status: 'identified',
message: 'Database connection pool is at capacity due to increased traffic',
author: 'Database Team'
},
{
id: 'upd-3',
timestamp: Date.now() - 30 * 60 * 1000,
status: 'monitoring',
message: 'Connection pool size has been increased. Monitoring for stability.',
author: 'Operations Team'
}
]
},
{
id: 'inc-002',
title: 'CDN Configuration Update',
status: 'identified',
severity: 'minor',
affectedServices: ['CDN', 'Static Assets'],
startTime: Date.now() - 45 * 60 * 1000,
impact: 'Some static assets may load slowly in certain regions',
updates: [
{
id: 'upd-4',
timestamp: Date.now() - 45 * 60 * 1000,
status: 'investigating',
message: 'Investigating reports of slow asset loading',
author: 'Network Team'
},
{
id: 'upd-5',
timestamp: Date.now() - 20 * 60 * 1000,
status: 'identified',
message: 'CDN configuration needs optimization for new edge locations',
author: 'Network Team'
}
]
}
];
const pastIncidents = [
{
id: 'inc-003',
title: 'Authentication Service Outage',
status: 'resolved',
severity: 'critical',
affectedServices: ['Authentication', 'All Services'],
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
impact: 'Users were unable to log in',
rootCause: 'Certificate expiration in auth service',
resolution: 'Certificate renewed and monitoring alerts added',
updates: [
{
id: 'upd-6',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Multiple reports of login failures',
author: 'On-call Engineer'
},
{
id: 'upd-7',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
status: 'identified',
message: 'SSL certificate has expired',
author: 'Security Team'
},
{
id: 'upd-8',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
status: 'resolved',
message: 'Certificate renewed and service restored',
author: 'Security Team'
}
]
}
];
// Set the incidents
incidents.currentIncidents = currentIncidents;
incidents.pastIncidents = pastIncidents;
// Pre-subscribe to the first incident
incidents.subscribedIncidentIds = ['inc-001'];
addLog('Pre-subscribed to incident: "Database Connection Pool Exhaustion"');
// Handle subscription events
incidents.addEventListener('incidentSubscribe', (event) => {
const { incidentId, incidentTitle, affectedServices } = event.detail;
addLog(`SUBSCRIBED to incident: "${incidentTitle}" (ID: ${incidentId})`);
addLog(` Affected services: ${affectedServices.join(', ')}`);
showNotification(`✓ Subscribed to: ${incidentTitle}`, 'subscribe');
// In a real application, you would send this to your backend
console.log('Subscribe API call would be made here:', {
incidentId,
userEmail: 'user@example.com',
notificationPreferences: ['email', 'sms']
});
});
incidents.addEventListener('incidentUnsubscribe', (event) => {
const { incident } = event.detail;
addLog(`UNSUBSCRIBED from incident: "${incident.title}" (ID: ${incident.id})`);
showNotification(`Unsubscribed from: ${incident.title}`, 'unsubscribe');
// In a real application, you would send this to your backend
console.log('Unsubscribe API call would be made here:', {
incidentId: incident.id,
userEmail: 'user@example.com'
});
});
// Simulate receiving a new update for a subscribed incident
setTimeout(() => {
if (incidents.subscribedIncidents && incidents.subscribedIncidents.has('inc-001')) {
addLog('📬 NEW UPDATE for subscribed incident "Database Connection Pool Exhaustion"');
showNotification('New update: Connection pool stable, incident will be resolved soon');
}
}, 5000);
});
</script>
</body>
</html>

175
test-shadcn-spacing.html Normal file
View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>shadcn-Aligned Spacing Test</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
.demo-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 24px; /* Using consistent spacing between components */
}
.spacing-info {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
font-size: 13px;
max-width: 300px;
z-index: 1000;
}
@media (prefers-color-scheme: dark) {
.spacing-info {
background: rgba(18, 18, 18, 0.9);
border-color: #27272a;
}
}
.spacing-info h3 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
}
.spacing-info ul {
margin: 0;
padding-left: 20px;
line-height: 1.6;
}
.spacing-info code {
background: #f3f4f6;
padding: 2px 4px;
border-radius: 3px;
font-size: 12px;
}
@media (prefers-color-scheme: dark) {
.spacing-info code {
background: #27272a;
}
}
</style>
</head>
<body>
<div class="demo-wrapper">
<upl-statuspage-header
page-title="CloudFlow"
show-report-button="true"
show-subscribe-button="true"
></upl-statuspage-header>
<upl-statuspage-pagetitle
page-title="Service Status"
page-subtitle="Current operational status of CloudFlow Infrastructure services"
></upl-statuspage-pagetitle>
<upl-statuspage-statusbar id="statusbar"></upl-statuspage-statusbar>
<upl-statuspage-statsgrid
current-status="operational"
uptime="99.95"
avg-response-time="125"
total-incidents="2"
></upl-statuspage-statsgrid>
<upl-statuspage-assetsselector id="assets"></upl-statuspage-assetsselector>
<upl-statuspage-statusdetails id="details"></upl-statuspage-statusdetails>
<upl-statuspage-statusmonth id="month"></upl-statuspage-statusmonth>
<upl-statuspage-incidents id="incidents"></upl-statuspage-incidents>
<upl-statuspage-footer
company-name="CloudFlow Infrastructure"
enable-subscribe="true"
subscriber-count="1234"
></upl-statuspage-footer>
</div>
<div class="spacing-info">
<h3>shadcn Spacing Improvements</h3>
<ul>
<li>Page title padding reduced from <code>48px</code> to <code>24px</code></li>
<li>Mini-heading padding reduced from <code>16px</code> to <code>8px</code></li>
<li>All arbitrary values replaced with spacing scale</li>
<li>Consistent <code>24px</code> gap between components</li>
<li>More compact, professional layout</li>
<li>Follows shadcn's minimal spacing approach</li>
</ul>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const statusbar = document.getElementById('statusbar');
const assets = document.getElementById('assets');
const details = document.getElementById('details');
const month = document.getElementById('month');
const incidents = document.getElementById('incidents');
// Configure status bar
statusbar.overallStatus = {
status: 'operational',
message: 'All systems operational',
lastUpdated: Date.now()
};
// Configure services
const services = [
{
id: 'api',
name: 'API Gateway',
displayName: 'API Gateway',
currentStatus: 'operational',
uptime30d: 99.99,
category: 'Core Services',
selected: true
},
{
id: 'database',
name: 'Database Cluster',
displayName: 'Database Cluster',
currentStatus: 'operational',
uptime30d: 99.95,
category: 'Core Services',
selected: true
}
];
assets.services = services;
details.services = services;
// Configure monthly data
month.monthlyData = [{
month: '2024-01',
days: Array(31).fill(null).map((_, i) => ({
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 100,
incidents: 0
})),
overallUptime: 100,
totalIncidents: 0
}];
// Configure incidents
incidents.currentIncidents = [];
incidents.pastIncidents = [];
});
</script>
</body>
</html>

107
test-statsgrid.html Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stats Grid Test</title>
<script type="module" src="./dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
}
@media (prefers-color-scheme: dark) {
body {
background: #09090b;
}
}
.test-section {
margin-bottom: 40px;
}
h2 {
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Stats Grid Component Test</h1>
<div class="test-section">
<h2>Normal Operation</h2>
<upl-statuspage-statsgrid
id="grid1"
current-status="operational"
uptime="99.95"
avg-response-time="125"
total-incidents="0"
affected-services="0"
total-services="12"
time-period="90 days"
></upl-statuspage-statsgrid>
</div>
<div class="test-section">
<h2>Degraded Performance</h2>
<upl-statuspage-statsgrid
id="grid2"
current-status="degraded"
uptime="98.50"
avg-response-time="450"
total-incidents="3"
affected-services="2"
total-services="12"
time-period="30 days"
></upl-statuspage-statsgrid>
</div>
<div class="test-section">
<h2>Major Outage</h2>
<upl-statuspage-statsgrid
id="grid3"
current-status="major_outage"
uptime="95.20"
avg-response-time="1250"
total-incidents="8"
affected-services="7"
total-services="12"
time-period="7 days"
></upl-statuspage-statsgrid>
</div>
<div class="test-section">
<h2>Loading State</h2>
<upl-statuspage-statsgrid
id="grid4"
loading="true"
></upl-statuspage-statsgrid>
</div>
<script>
// Test dynamic updates
setTimeout(() => {
const grid1 = document.getElementById('grid1');
console.log('Updating grid1 to show degraded status...');
grid1.currentStatus = 'degraded';
grid1.avgResponseTime = 350;
grid1.totalIncidents = 1;
grid1.affectedServices = 1;
}, 3000);
// Test loading state toggle
setTimeout(() => {
const grid4 = document.getElementById('grid4');
console.log('Loading complete, showing data...');
grid4.loading = false;
grid4.currentStatus = 'operational';
grid4.uptime = 99.99;
grid4.avgResponseTime = 85;
grid4.totalIncidents = 0;
grid4.affectedServices = 0;
grid4.totalServices = 15;
grid4.timePeriod = '24 hours';
}, 5000);
</script>
</body>
</html>

1
test/test.ts Normal file
View File

@@ -0,0 +1 @@
console.log('hello');

View File

@@ -1,8 +1,8 @@
/** /**
* autocreated commitinfo by @pushrocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@uptimelink_private/catalog', name: '@uptime.link/statuspage',
version: '1.0.71', version: '1.2.0',
description: 'a catalog with webcomponents for uptimelink dashboard' description: 'A catalog of web components for the UptimeLink dashboard.'
} }

View File

@@ -1,7 +1,13 @@
// Export components
export * from './upl-statuspage-assetsselector.js'; export * from './upl-statuspage-assetsselector.js';
export * from './upl-statuspage-footer.js'; export * from './upl-statuspage-footer.js';
export * from './upl-statuspage-header.js'; export * from './upl-statuspage-header.js';
export * from './upl-statuspage-incidents.js'; export * from './upl-statuspage-incidents.js';
export * from './upl-statuspage-pagetitle.js';
export * from './upl-statuspage-statusbar.js'; export * from './upl-statuspage-statusbar.js';
export * from './upl-statuspage-statusdetails.js'; export * from './upl-statuspage-statusdetails.js';
export * from './upl-statuspage-statusmonth.js'; export * from './upl-statuspage-statusmonth.js';
export * from './upl-statuspage-statsgrid.js';
// Export interfaces
export * from '../interfaces/index.js';

View File

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

@@ -3,13 +3,17 @@ import {
property, property,
html, html,
customElement, customElement,
TemplateResult, type TemplateResult,
cssManager, cssManager,
css, css,
} from '@designestate/dees-element'; unsafeCSS,
import * as domtools from '@designestate/dees-domtools'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IServiceStatus } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js'; import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -19,9 +23,25 @@ declare global {
@customElement('upl-statuspage-assetsselector') @customElement('upl-statuspage-assetsselector')
export class UplStatuspageAssetsselector extends DeesElement { export class UplStatuspageAssetsselector extends DeesElement {
public static demo = () => html` public static demo = demoFunc;
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
`; @property({ type: Array })
accessor services: IServiceStatus[] = [];
@property({ type: String })
accessor filterText: string = '';
@property({ type: String })
accessor filterCategory: string = 'all';
@property({ type: Boolean })
accessor showOnlySelected: boolean = false;
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Boolean })
accessor expanded: boolean = false;
constructor() { constructor() {
super(); super();
@@ -29,35 +49,597 @@ export class UplStatuspageAssetsselector extends DeesElement {
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
sharedStyles.commonStyles,
css` css`
:host { :host {
padding: 0px 0px 15px 0px;
display: block; display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')}; background: transparent;
font-family: Inter; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: #fff; color: ${sharedStyles.colors.text.primary};
} }
.mainbox { .container {
margin: auto; max-width: 1200px;
max-width: 900px; margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.controls {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
flex-wrap: wrap;
align-items: center;
}
.search-input {
flex: 1;
min-width: 200px;
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
background: ${sharedStyles.colors.background.card};
color: ${sharedStyles.colors.text.primary};
font-size: 13px;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
height: 32px;
}
.search-input:focus {
outline: none;
border-color: ${sharedStyles.colors.text.primary};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.search-input::placeholder {
color: ${sharedStyles.colors.text.muted};
}
.filter-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
background: ${sharedStyles.colors.background.card};
color: ${sharedStyles.colors.text.secondary};
cursor: pointer;
font-size: 13px;
font-weight: 500;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
height: 32px;
}
.filter-button:hover {
border-color: ${sharedStyles.colors.border.muted};
color: ${sharedStyles.colors.text.primary};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
.filter-button.active {
background: ${sharedStyles.colors.text.primary};
color: ${sharedStyles.colors.background.primary};
border-color: ${sharedStyles.colors.text.primary};
}
.selected-services {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
align-items: center;
}
.service-pill {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
font-size: 13px;
font-weight: 500;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
animation: pillFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.service-pill:nth-child(1) { animation-delay: 0ms; }
.service-pill:nth-child(2) { animation-delay: 30ms; }
.service-pill:nth-child(3) { animation-delay: 60ms; }
.service-pill:nth-child(4) { animation-delay: 90ms; }
.service-pill:nth-child(5) { animation-delay: 120ms; }
@keyframes pillFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.service-pill:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-1px);
}
.service-pill .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.service-pill .status-dot.operational {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(34, 197, 94, 0.2)', 'rgba(34, 197, 94, 0.3)')};
}
.manage-button {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
color: ${sharedStyles.colors.text.secondary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.manage-button:hover {
border-color: ${sharedStyles.colors.border.muted};
color: ${sharedStyles.colors.text.primary};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-1px);
}
.manage-button:active {
transform: translateY(0);
}
.expandable-section {
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
overflow: hidden;
animation: expandIn 0.3s ${unsafeCSS(sharedStyles.easings.default)};
}
@keyframes expandIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.expandable-content {
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
}
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
}
.asset-card {
display: flex;
align-items: center;
padding: ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
border: 1px solid ${sharedStyles.colors.border.default};
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
animation: cardFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.asset-card:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-2px);
}
.asset-card.selected {
border-color: ${sharedStyles.colors.text.primary};
background: ${sharedStyles.colors.background.secondary};
}
.asset-card.selected:hover {
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
}
.asset-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: ${sharedStyles.colors.text.primary};
flex-shrink: 0;
}
.asset-info {
flex: 1;
min-width: 0;
}
.asset-name {
font-weight: 600;
font-size: 14px;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-description {
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-status {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
flex-shrink: 0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
flex-shrink: 0;
}
.status-indicator.operational, .status-dot.operational {
background: ${sharedStyles.colors.status.operational};
}
.status-indicator.degraded, .status-dot.degraded {
background: ${sharedStyles.colors.status.degraded};
}
.status-indicator.partial_outage, .status-dot.partial_outage {
background: ${sharedStyles.colors.status.partial};
}
.status-indicator.major_outage, .status-dot.major_outage {
background: ${sharedStyles.colors.status.major};
}
.status-indicator.maintenance, .status-dot.maintenance {
background: ${sharedStyles.colors.status.maintenance};
}
.status-text {
font-size: 12px;
text-transform: capitalize;
color: ${sharedStyles.colors.text.secondary};
}
.loading-message,
.no-results {
grid-column: 1 / -1;
text-align: center; text-align: center;
height: 50px; padding: ${unsafeCSS(sharedStyles.spacing.xl)};
border-radius: 3px; color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
background: ${cssManager.bdTheme('#ffffff', '#333333')};; font-size: 13px;
}
.summary {
text-align: right;
font-size: 12px;
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
}
.no-services {
padding: ${unsafeCSS(sharedStyles.spacing.xl)};
text-align: center;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
font-size: 13px;
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.controls {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
}
.selected-services {
flex-direction: column;
align-items: stretch;
}
.service-pill {
width: auto;
}
.expandable-content {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
}
.assets-grid {
grid-template-columns: 1fr;
}
.asset-card {
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
}
} }
`, `,
] ]
public render(): TemplateResult { public render(): TemplateResult {
const selectedServices = this.services.filter(s => s.selected);
const selectedCount = selectedServices.length;
const categories = this.getUniqueCategories();
return html` return html`
<style> <div class="container">
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
</style> <div class="selected-services">
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading> ${selectedCount === 0 ? html`
<div class="mainbox"> <span style="color: ${cssManager.bdTheme('#9ca3af', '#71717a')}; font-size: 13px;">
Hello! No services selected
</span>
` : selectedCount > 5 && !this.expanded ? html`
${selectedServices.slice(0, 4).map(service => html`
<div class="service-pill">
<span class="status-dot ${service.currentStatus}"></span>
<span>${service.displayName}</span>
</div>
`)}
<span style="color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')}; font-size: 12px;">
+${selectedCount - 4} more
</span>
` : selectedServices.map(service => html`
<div class="service-pill">
<span class="status-dot ${service.currentStatus}"></span>
<span>${service.displayName}</span>
</div>
`)}
<button
class="manage-button"
@click=${() => { this.expanded = !this.expanded; }}
>
${this.expanded ? 'Close' : 'Manage Services'}
<svg
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="transform: rotate(${this.expanded ? '180deg' : '0'}); transition: transform 0.2s;"
>
<path
d="M1 1L5 5L9 1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
${this.expanded ? html`
<div class="expandable-section">
<div class="expandable-content">
<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>
` : this.renderServiceGrid()}
</div>
<div class="summary">
${selectedCount} of ${this.services.length} services selected
</div>
</div>
</div>
` : ''}
</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
}));
}
private renderServiceGrid(): TemplateResult {
const filteredServices = this.getFilteredServices();
if (filteredServices.length === 0) {
return html`<div class="no-results">No services found matching your criteria</div>`;
}
return html`${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>
`)}`;
}
} }

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, TemplateResult, css, cssManager } from '@designestate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@designestate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import * as sharedStyles from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-footer.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -10,18 +12,80 @@ declare global {
@customElement('upl-statuspage-footer') @customElement('upl-statuspage-footer')
export class UplStatuspageFooter extends DeesElement { export class UplStatuspageFooter extends DeesElement {
// STATIC // STATIC
public static demo = () => html` public static demo = demoFunc;
<upl-statuspage-footer></upl-statuspage-footer>
`;
// INSTANCE // INSTANCE
@property() @property({ type: String })
public legalInfo: string = "https://lossless.gmbh"; accessor companyName: string = '';
@property({ @property({ type: String })
type: Boolean accessor legalUrl: string = '';
})
public whitelabel = false; @property({ type: String })
accessor supportEmail: string = '';
@property({ type: String })
accessor statusPageUrl: string = '';
@property({ type: Boolean })
accessor whitelabel: boolean = false;
@property({ type: Number })
accessor lastUpdated: number | null = null;
@property({ type: Number })
accessor currentYear: number = new Date().getFullYear();
@property({ type: Array })
accessor socialLinks: Array<{ platform: string; url: string }> = [];
@property({ type: Array })
accessor additionalLinks: Array<{ label: string; url: string }> = [];
@property({ type: String })
accessor rssFeedUrl: string = '';
@property({ type: String })
accessor apiStatusUrl: string = '';
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: String })
accessor errorMessage: string | null = null;
@property({ type: Boolean })
accessor offline: boolean = false;
@property({ type: String })
accessor latestStatusUpdate: string = '';
@property({ type: Boolean })
accessor enableSubscribe: boolean = false;
@property({ type: Boolean })
accessor enableReportIssue: boolean = false;
@property({ type: Boolean })
accessor enableLanguageSelector: boolean = false;
@property({ type: Boolean })
accessor enableThemeToggle: boolean = false;
@property({ type: Array })
accessor languageOptions: Array<{ code: string; label: string }> = [];
@property({ type: String })
accessor currentLanguage: string = 'en';
@property({ type: String })
accessor currentTheme: string = 'light';
@property({ type: Number })
accessor subscriberCount: number = 0;
@property({ type: Object })
accessor customBranding: { primaryColor?: string; logoUrl?: string; footerText?: string } | null = null;
constructor() { constructor() {
@@ -30,43 +94,611 @@ export class UplStatuspageFooter extends DeesElement {
public static styles = [ public static styles = [
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css` css`
:host { :host {
display: block; display: block;
background: ${cssManager.bdTheme('#ffffff', '#000000')}; background: ${sharedStyles.colors.background.primary};
font-family: Inter; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${cssManager.bdTheme('#333333', '#ffffff')}; color: ${sharedStyles.colors.text.primary};
font-size: 14px;
border-top: 1px solid ${sharedStyles.colors.border.default};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: ${unsafeCSS(sharedStyles.spacing['2xl'])} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.footer-content {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.xl)};
}
.footer-main {
display: flex;
justify-content: space-between;
align-items: start;
gap: ${unsafeCSS(sharedStyles.spacing['2xl'])};
}
.company-info {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.company-name {
font-size: 16px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
letter-spacing: -0.01em;
}
.company-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.footer-link {
color: ${sharedStyles.colors.text.secondary};
text-decoration: none;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
font-size: 13px;
font-weight: 400;
position: relative;
}
.footer-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: ${sharedStyles.colors.text.primary};
transition: width ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.footer-link:hover {
color: ${sharedStyles.colors.text.primary};
}
.footer-link:hover::after {
width: 100%;
}
.footer-actions {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.action-button {
padding: 8px 16px;
height: 36px;
border: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.card};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
text-align: center;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
white-space: nowrap;
font-size: 13px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.action-button:hover {
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.muted};
transform: translateY(-1px);
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
.action-button:active {
transform: translateY(0);
box-shadow: none;
}
.footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
}
.footer-meta {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
align-items: center;
flex-wrap: wrap;
}
.social-links {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
align-items: center;
}
.social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
color: ${sharedStyles.colors.text.muted};
border: 1px solid transparent;
}
.social-link:hover {
color: ${sharedStyles.colors.text.primary};
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.default};
transform: translateY(-2px);
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
.social-link:active {
transform: translateY(0);
}
.social-link svg {
width: 16px;
height: 16px;
fill: currentColor;
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.social-link:hover svg {
transform: scale(1.1);
}
.copyright {
font-size: 13px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
}
.last-updated {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
}
.powered-by {
font-size: 12px;
color: ${sharedStyles.colors.text.muted};
text-align: right;
}
.powered-by a {
color: ${sharedStyles.colors.text.secondary};
text-decoration: none;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
font-weight: 500;
position: relative;
}
.powered-by a::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 1px;
background: ${sharedStyles.colors.text.primary};
transition: width ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.powered-by a:hover {
color: ${sharedStyles.colors.text.primary};
}
.powered-by a:hover::after {
width: 100%;
}
.status-update {
padding: 12px 16px;
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px;
font-size: 13px;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
line-height: 1.5;
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
}
.language-selector {
position: relative;
}
.language-selector select {
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.primary};
color: ${sharedStyles.colors.text.primary};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 14px;
cursor: pointer;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.theme-toggle {
cursor: pointer;
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
background: transparent;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 14px;
transition: all 0.2s ease;
color: ${sharedStyles.colors.text.primary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.theme-toggle:hover {
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.muted};
}
.subscribe-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
}
.subscriber-count {
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
}
.error-message {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
background: ${cssManager.bdTheme('#fee9e9', '#7f1d1d')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
font-size: 14px;
border: 1px solid ${cssManager.bdTheme('#fecaca', '#991b1b')};
}
.offline-indicator {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.status.degraded};
color: white;
border-radius: ${unsafeCSS(sharedStyles.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(sharedStyles.borderRadius.md)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.additional-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
}
.additional-link {
font-size: 13px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
text-decoration: none;
transition: color 0.15s ease;
}
.additional-link:hover {
color: ${sharedStyles.colors.text.primary};
}
@media (max-width: 640px) {
.container {
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.md)};
} }
.mainbox { .footer-main {
max-width: 900px; flex-direction: column;
margin: auto; gap: ${unsafeCSS(sharedStyles.spacing.lg)};
padding-top: 20px;
padding-bottom: 20px;
} }
.footer-bottom {
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
align-items: start;
}
.footer-meta {
flex-direction: column;
align-items: start;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.company-links {
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.powered-by {
text-align: left;
}
}
` `
] ]
public render(): TemplateResult { public render(): TemplateResult {
if (this.loading) {
return html`
<div class="container">
<div class="loading-skeleton"></div>
</div>
`;
}
return html` return html`
${domtools.elementBasic.styles} <div class="container">
<style></style> <div class="footer-content">
<div class="mainbox"> ${this.errorMessage ? html`
Hi there <div class="error-message">${this.errorMessage}</div>
` : ''}
${this.offline ? html`
<div class="offline-indicator">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2V6M6 10H6.01M11 6C11 8.76142 8.76142 11 6 11C3.23858 11 1 8.76142 1 6C1 3.23858 3.23858 1 6 1C8.76142 1 11 3.23858 11 6Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
You are currently offline
</div>
` : ''}
${this.latestStatusUpdate ? html`
<div class="status-update">
Latest Update: ${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`
<div class="subscribe-wrapper">
<button class="action-button" @click=${this.handleSubscribeClick}>
Subscribe to Updates
</button>
${this.subscriberCount > 0 ? html`
<div class="subscriber-count">${this.subscriberCount.toLocaleString()} subscribers</div>
` : ''}
</div>
` : ''}
${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> </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() { public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', { this.handleReportIncidentClick();
}))
} }
public dispatchStatusSubscribe() { 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, TemplateResult, css, cssManager } from '@designestate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@designestate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import * as sharedStyles from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-header.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -10,13 +12,23 @@ declare global {
@customElement('upl-statuspage-header') @customElement('upl-statuspage-header')
export class UplStatuspageHeader extends DeesElement { export class UplStatuspageHeader extends DeesElement {
// STATIC // STATIC
public static demo = () => html` public static demo = demoFunc;
<upl-statuspage-header></upl-statuspage-header>
`;
// INSTANCE // INSTANCE
@property() @property({ type: String })
public pageTitle: string = "Statuspage Title"; accessor pageTitle: string = "Statuspage Title";
@property({ type: Boolean })
accessor showReportButton: boolean = true;
@property({ type: Boolean })
accessor showSubscribeButton: boolean = true;
@property({ type: String })
accessor logoUrl: string = '';
@property({ type: Boolean })
accessor loading: boolean = false;
constructor() { constructor() {
@@ -27,72 +39,297 @@ export class UplStatuspageHeader extends DeesElement {
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
css` css`
:host { :host {
display: block; display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')}; background: ${cssManager.bdTheme(
font-family: Inter; 'rgba(255, 255, 255, 0.85)',
color: ${cssManager.bdTheme('#333333', '#ffffff')}; 'rgba(9, 9, 11, 0.85)'
)};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.primary};
border-bottom: 1px solid ${cssManager.bdTheme(
'rgba(0, 0, 0, 0.06)',
'rgba(255, 255, 255, 0.06)'
)};
position: sticky;
top: 0;
z-index: 40;
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
}
.header-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)};
}
.header-nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}
.header-left {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.header-actions {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.actionButton {
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
font-size: 13px;
font-weight: 500;
padding: 0 14px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
user-select: none;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
background: transparent;
border: 1px solid ${sharedStyles.colors.border.default};
color: ${sharedStyles.colors.text.primary};
letter-spacing: -0.01em;
position: relative;
overflow: hidden;
}
.actionButton::before {
content: '';
position: absolute;
inset: 0;
background: ${cssManager.bdTheme(
'linear-gradient(135deg, rgba(0, 0, 0, 0.02) 0%, transparent 100%)',
'linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, transparent 100%)'
)};
opacity: 0;
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.actionButton:hover {
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-1px);
}
.actionButton:hover::before {
opacity: 1;
}
.actionButton:active {
transform: translateY(0) scale(0.98);
transition-duration: ${unsafeCSS(sharedStyles.durations.fast)};
box-shadow: none;
}
.actionButton:focus-visible {
outline: 2px solid ${sharedStyles.colors.accent.focus};
outline-offset: 2px;
}
/* Button icon styles */
.actionButton svg {
width: 14px;
height: 14px;
flex-shrink: 0;
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.actionButton:hover svg {
transform: scale(1.1);
}
.site-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.02em;
color: ${sharedStyles.colors.text.primary};
transition: color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.site-title:hover {
color: ${sharedStyles.colors.text.secondary};
}
.logo {
height: 28px;
width: auto;
filter: ${cssManager.bdTheme('none', 'brightness(0) invert(1)')};
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
}
.logo:hover {
opacity: 0.8;
transform: scale(1.02);
}
.page-info {
padding: ${unsafeCSS(sharedStyles.spacing.lg)} 0 ${unsafeCSS(sharedStyles.spacing.xl)} 0;
}
.page-title {
font-size: 48px;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
color: ${sharedStyles.colors.text.primary};
margin: 0 0 16px 0;
}
.page-subtitle {
font-size: 18px;
color: ${sharedStyles.colors.text.secondary};
margin: 0;
line-height: 1.5;
}
/* Primary button variant */
.actionButton.primary {
background: ${sharedStyles.colors.accent.primary};
color: ${sharedStyles.colors.background.primary};
border-color: transparent;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.actionButton.primary::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, transparent 100%);
}
.actionButton.primary:hover {
background: ${sharedStyles.colors.accent.hover};
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
}
.loading-skeleton {
height: 64px;
background: ${sharedStyles.colors.background.secondary};
border-bottom: 1px solid ${sharedStyles.colors.border.default};
position: relative;
overflow: hidden;
}
.loading-skeleton::after {
content: '';
position: absolute;
inset: 0;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.04) 50%, transparent 100%)',
'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%)'
)};
animation: shimmer 1.5s ${unsafeCSS(sharedStyles.easings.default)} infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
@media (max-width: 768px) {
.header-container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)};
} }
.mainbox { .header-nav {
margin: auto; height: 56px;
max-width: 900px;
} }
.mainbox .actions { .header-left {
display: flex; gap: ${unsafeCSS(sharedStyles.spacing.sm)};
justify-content: flex-end;
padding: 20px 0px 40px 0px;
} }
.mainbox .actions .actionButton { .site-title {
background: ${cssManager.bdTheme('#00000000', '#ffffff00')}; font-size: 15px;
}
.logo {
height: 24px;
}
}
@media (max-width: 640px) {
.actionButton {
font-size: 12px; font-size: 12px;
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')}; padding: 0 10px;
padding: 6px 10px 7px 10px; height: 32px;
margin-left: 10px; gap: 4px;
border-radius: 3px;
cursor: pointer;
user-select: none;
} }
.mainbox .actions .actionButton:hover { .actionButton svg {
background: ${cssManager.bdTheme('#333333', '#efefef')}; width: 12px;
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')}; height: 12px;
color: ${cssManager.bdTheme('#fff', '#333333')};
} }
h1 { .actionButton .button-text {
margin: 0px; display: none;
text-align: center;
font-weight: 600;
font-size: 35px;
} }
h2 { .page-title {
margin: 0px; font-size: 32px;
margin-top: 10px;
text-align: center;
font-weight: 600;
font-size: 18px;
} }
.page-subtitle {
font-size: 16px;
}
.header-actions {
gap: 6px;
}
}
` `
] ]
public render(): TemplateResult { public render(): TemplateResult {
if (this.loading) {
return html`
<div class="loading-skeleton"></div>
`;
}
return html` return html`
${domtools.elementBasic.styles} <header>
<style> <div class="header-container">
<nav class="header-nav">
</style> <div class="header-left">
<div class="mainbox"> ${this.logoUrl ? html`
<div class="actions"> <img src="${this.logoUrl}" alt="${this.pageTitle}" class="logo">
<div class="actionButton" @click=${this.dispatchReportNewIncident}>report new incident</div> ` : ''}
<div class="actionButton" @click=${this.dispatchStatusSubscribe}>subscribe</div> <h1 class="site-title">${this.pageTitle}</h1>
</div>
<div class="header-actions">
${this.showReportButton ? html`
<button class="actionButton" @click=${this.dispatchReportNewIncident}>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86L7.86 2z"></path>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span class="button-text">Report Issue</span>
</button>
` : ''}
${this.showSubscribeButton ? html`
<button class="actionButton primary" @click=${this.dispatchStatusSubscribe}>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>
</svg>
<span class="button-text">Subscribe</span>
</button>
` : ''}
</div>
</nav>
</div> </div>
<h1>${this.pageTitle}</h1> </header>
<h2>STATUS BOARD</h2>
</div>
`; `;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,15 @@ import {
property, property,
html, html,
customElement, customElement,
TemplateResult, type TemplateResult,
css, css,
cssManager, cssManager,
} from '@designestate/dees-element'; unsafeCSS,
} from '@design.estate/dees-element';
import type { IIncidentDetails } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-incidents.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -18,73 +23,882 @@ declare global {
@customElement('upl-statuspage-incidents') @customElement('upl-statuspage-incidents')
export class UplStatuspageIncidents extends DeesElement { export class UplStatuspageIncidents extends DeesElement {
// STATIC // STATIC
public static demo = () => html` <upl-statuspage-incidents></upl-statuspage-incidents> `; public static demo = demoFunc;
// INSTANCE // INSTANCE
@property({ @property({
type: Array, type: Array,
}) })
public currentIncidences: plugins.uplInterfaces.data.IIncident[] = []; accessor currentIncidents: IIncidentDetails[] = [];
@property({ @property({
type: Array, type: Array,
}) })
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = []; accessor pastIncidents: IIncidentDetails[] = [];
@property({ @property({
type: Boolean, type: Boolean,
}) })
public whitelabel = false; accessor whitelabel = false;
@property({
type: Boolean,
})
accessor loading = false;
@property({
type: Number,
})
accessor daysToShow = 90;
@property({
type: Array,
})
accessor subscribedIncidentIds: string[] = [];
@property({
type: Object,
state: true,
})
private accessor expandedIncidents: Set<string> = new Set();
@property({
type: Object,
state: true,
})
private accessor subscribedIncidents: Set<string> = new Set();
constructor() { constructor() {
super(); super();
} }
async connectedCallback() {
await super.connectedCallback();
// Initialize subscribed incidents from the property
if (this.subscribedIncidentIds.length > 0) {
this.subscribedIncidents = new Set(this.subscribedIncidentIds);
}
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('subscribedIncidentIds')) {
this.subscribedIncidents = new Set(this.subscribedIncidentIds);
}
}
public static styles = [ public static styles = [
plugins.domtools.elementBasic.staticStyles, plugins.domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css` css`
:host { :host {
display: block; display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')}; background: transparent;
font-family: Inter; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${cssManager.bdTheme('#333333', '#ffffff')}; color: ${sharedStyles.colors.text.primary};
} }
.mainbox { .container {
max-width: 900px; max-width: 1200px;
margin: auto; margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
} }
.noIncidentBox { .noIncidentBox {
background: ${cssManager.bdTheme('#ffffff', '#333333')};; background: ${sharedStyles.colors.background.card};
padding: 10px; padding: ${unsafeCSS(sharedStyles.spacing.xl)};
margin-bottom: 15px; margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
border-radius: 3px; border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
border: 1px solid ${sharedStyles.colors.border.default};
text-align: center;
color: ${sharedStyles.colors.text.secondary};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
}
/* Staggered entrance animations */
.incident-card {
background: ${sharedStyles.colors.background.card};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
overflow: hidden;
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.incident-card:nth-child(1) { animation-delay: 0ms; }
.incident-card:nth-child(2) { animation-delay: 50ms; }
.incident-card:nth-child(3) { animation-delay: 100ms; }
.incident-card:nth-child(4) { animation-delay: 150ms; }
.incident-card:nth-child(5) { animation-delay: 200ms; }
.incident-card:hover {
transform: translateY(-2px);
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
border-color: ${sharedStyles.colors.border.muted};
}
.incident-card.expanded {
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
}
/* Active incident pulse effect */
.incident-card.active-incident {
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both,
incident-pulse 3s ease-in-out infinite;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes incident-pulse {
0%, 100% {
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
50% {
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)},
0 0 0 2px ${cssManager.bdTheme('rgba(239, 68, 68, 0.15)', 'rgba(248, 113, 113, 0.2)')};
}
}
.incident-header {
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.xl)};
border-left: 4px solid;
display: flex;
align-items: start;
justify-content: space-between;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
cursor: pointer;
transition: background-color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
}
.incident-header:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')};
}
.incident-header.critical {
border-left-color: ${sharedStyles.colors.status.major};
}
.incident-header.major {
border-left-color: ${sharedStyles.colors.status.partial};
}
.incident-header.minor {
border-left-color: ${sharedStyles.colors.status.degraded};
}
.incident-header.maintenance {
border-left-color: ${sharedStyles.colors.status.maintenance};
}
.incident-title {
font-size: 17px;
font-weight: 600;
margin: 0;
line-height: 1.4;
letter-spacing: -0.01em;
}
.incident-meta {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
flex-wrap: wrap;
}
.incident-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.incident-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
flex-shrink: 0;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.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')};
}
/* Pulse for investigating status */
.incident-status.investigating .status-dot {
animation: status-pulse 1.5s ease-in-out infinite;
}
@keyframes status-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
}
.incident-body {
padding: 0 ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)};
animation: slideDown 0.3s ${unsafeCSS(sharedStyles.easings.default)};
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.incident-impact {
margin: ${unsafeCSS(sharedStyles.spacing.md)} 0;
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
background: ${sharedStyles.colors.background.secondary};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 14px;
line-height: 1.6;
border-left: 3px solid ${sharedStyles.colors.border.muted};
}
.affected-services {
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.affected-services-title {
font-size: 12px;
font-weight: 600;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
color: ${sharedStyles.colors.text.secondary};
text-transform: uppercase;
letter-spacing: 0.04em;
}
.service-tag {
display: inline-block;
padding: 4px 10px;
margin: 2px;
background: ${sharedStyles.colors.background.muted};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
font-size: 12px;
color: ${sharedStyles.colors.text.secondary};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.service-tag:hover {
background: ${sharedStyles.colors.background.secondary};
color: ${sharedStyles.colors.text.primary};
}
/* Timeline visualization for updates */
.incident-updates {
margin-top: ${unsafeCSS(sharedStyles.spacing.xl)};
border-top: 1px solid ${sharedStyles.colors.border.default};
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.updates-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
margin: 0 0 ${unsafeCSS(sharedStyles.spacing.lg)} 0;
color: ${sharedStyles.colors.text.secondary};
}
.timeline {
position: relative;
padding-left: 24px;
}
/* Vertical connector line */
.timeline::before {
content: '';
position: absolute;
left: 5px;
top: 8px;
bottom: 8px;
width: 2px;
background: ${cssManager.bdTheme(
'linear-gradient(to bottom, #e5e7eb 0%, #d1d5db 50%, #e5e7eb 100%)',
'linear-gradient(to bottom, #27272a 0%, #3f3f46 50%, #27272a 100%)'
)};
border-radius: 1px;
}
.update-item {
position: relative;
padding-left: ${unsafeCSS(sharedStyles.spacing.lg)};
padding-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
animation: fadeInUp 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.update-item:last-child {
padding-bottom: 0;
}
/* Timeline dot */
.update-item::before {
content: '';
position: absolute;
left: -22px;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: ${sharedStyles.colors.background.card};
border: 2px solid ${sharedStyles.colors.border.muted};
z-index: 1;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.update-item:first-child::before {
border-color: ${sharedStyles.colors.status.operational};
background: ${sharedStyles.colors.status.operational};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(22, 163, 74, 0.15)', 'rgba(34, 197, 94, 0.2)')};
}
.update-item:hover::before {
transform: scale(1.2);
border-color: ${sharedStyles.colors.text.secondary};
}
.update-time {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
margin-bottom: 4px;
font-family: ${unsafeCSS(sharedStyles.fonts.mono)};
display: flex;
align-items: center;
gap: 8px;
}
.update-status-badge {
display: inline-flex;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
background: ${sharedStyles.colors.background.muted};
color: ${sharedStyles.colors.text.secondary};
}
.update-message {
font-size: 14px;
line-height: 1.6;
color: ${sharedStyles.colors.text.primary};
}
.update-author {
font-size: 12px;
color: ${sharedStyles.colors.text.muted};
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.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: shimmer 1.5s infinite;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.show-more {
text-align: center;
margin-top: ${unsafeCSS(sharedStyles.spacing.xl)};
}
.show-more-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
background: transparent;
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
color: ${sharedStyles.colors.text.primary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.show-more-button:hover {
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.muted};
transform: translateY(-2px);
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
.show-more-button:active {
transform: translateY(0);
}
.incident-actions {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
align-items: center;
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
border-top: 1px solid ${sharedStyles.colors.border.default};
}
.subscribe-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: transparent;
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
color: ${sharedStyles.colors.text.primary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.subscribe-button:hover {
background: ${sharedStyles.colors.background.secondary};
border-color: ${sharedStyles.colors.border.muted};
transform: translateY(-1px);
}
.subscribe-button.subscribed {
background: ${cssManager.bdTheme('#f0fdf4', '#064e3b')};
border-color: ${cssManager.bdTheme('#86efac', '#047857')};
color: ${cssManager.bdTheme('#047857', '#86efac')};
}
.subscribe-button.subscribed:hover {
background: ${cssManager.bdTheme('#dcfce7', '#065f46')};
}
.collapsed-hint {
font-size: 12px;
color: ${sharedStyles.colors.text.secondary};
text-align: center;
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
opacity: 0.8;
}
/* Expand icon animation */
.expand-icon {
transition: transform ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
}
.expand-icon.rotated {
transform: rotate(180deg);
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.incident-header {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
}
.incident-body {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.incident-meta {
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
}
.timeline {
padding-left: 20px;
}
.timeline::before {
left: 4px;
}
.update-item::before {
left: -18px;
width: 10px;
height: 10px;
}
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<style></style> <div class="container">
<div class="mainbox"> <uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
<uplinternal-miniheading> Current Incidents </uplinternal-miniheading> ${this.loading ? html`
${this.currentIncidences.length <div class="loading-skeleton"></div>
? html`` ` : this.currentIncidents.length ? html`
: html` <div class="noIncidentBox">No incidents ongoing.</div> `} ${this.currentIncidents.map(incident => this.renderIncident(incident, true))}
<uplinternal-miniheading> Past Incidents </uplinternal-miniheading> ` :
${this.pastIncidences.length html`<div class="noIncidentBox">No incidents ongoing.</div>`
? html`` }
: html` <div class="noIncidentBox">No past incidents in the last 90 days.</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> </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);
const isActive = isCurrent && latestUpdate?.status !== 'resolved';
return html`
<div class="incident-card ${this.expandedIncidents.has(incident.id) ? 'expanded' : ''} ${isActive ? 'active-incident' : ''}">
<div class="incident-header ${incident.severity}" @click=${() => this.toggleIncident(incident.id)}>
<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>
${!this.expandedIncidents.has(incident.id) ? html`
<div style="
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
">
${incident.impact ? html`
<span style="
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 500px;
">${incident.impact}</span>
` : ''}
<span style="
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
">
${incident.updates.length} update${incident.updates.length !== 1 ? 's' : ''}
</span>
</div>
` : ''}
</div>
<div style="display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.md)};">
<div class="incident-status ${latestUpdate.status}">
${this.getStatusIcon(latestUpdate.status)}
${latestUpdate.status.replace(/_/g, ' ')}
</div>
<div class="expand-icon" style="
font-size: 10px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
transition: transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
${this.expandedIncidents.has(incident.id) ? 'transform: rotate(180deg);' : ''}
">
</div>
</div>
</div>
${this.expandedIncidents.has(incident.id) ? html`
<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 class="updates-title">Updates</h4>
<div class="timeline">
${incident.updates.slice(-3).reverse().map((update, index) => this.renderUpdate(update, index))}
</div>
</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 class="incident-actions">
<button
class="subscribe-button ${this.isSubscribedToIncident(incident.id) ? 'subscribed' : ''}"
@click=${(e: Event) => {
e.stopPropagation();
this.handleIncidentSubscribe(incident);
}}
>
${this.isSubscribedToIncident(incident.id) ? html`
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6667 3.5L5.25 9.91667L2.33334 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Subscribed to updates
` : html`
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 5.25V8.75C10.5 9.34674 10.2629 9.91903 9.84099 10.341C9.41903 10.7629 8.84674 11 8.25 11L3.75 11C3.15326 11 2.58097 10.7629 2.15901 10.341C1.73705 9.91903 1.5 9.34674 1.5 8.75V4.25C1.5 3.65326 1.73705 3.08097 2.15901 2.65901C2.58097 2.23705 3.15326 2 3.75 2H7.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 1.5H12.5M12.5 1.5V5M12.5 1.5L6 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Subscribe to updates
`}
</button>
${isCurrent ? html`
<span style="
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
">Get notified when this incident is updated or resolved</span>
` : ''}
</div>
</div>
` : ''}
</div>
`;
}
private renderUpdate(update: any, index: number = 0): TemplateResult {
return html`
<div class="update-item" style="animation-delay: ${index * 50}ms">
<div class="update-time">
${this.formatDate(update.timestamp)}
${update.status ? html`<span class="update-status-badge">${update.status}</span>` : ''}
</div>
<div class="update-message">${update.message}</div>
${update.author ? html`
<div class="update-author">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
${update.author}
</div>
` : ''}
</div>
`;
}
private getStatusIcon(status: string): TemplateResult {
return html`<span class="status-dot" style="
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: ${status === 'resolved' ? sharedStyles.colors.status.operational :
status === 'monitoring' ? sharedStyles.colors.status.maintenance :
status === 'identified' ? sharedStyles.colors.status.degraded :
sharedStyles.colors.status.partial};
"></span>`;
}
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 toggleIncident(incidentId: string) {
const newExpanded = new Set(this.expandedIncidents);
if (newExpanded.has(incidentId)) {
newExpanded.delete(incidentId);
} else {
newExpanded.add(incidentId);
}
this.expandedIncidents = newExpanded;
}
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');
}
private isSubscribedToIncident(incidentId: string): boolean {
return this.subscribedIncidents.has(incidentId);
}
private handleIncidentSubscribe(incident: IIncidentDetails) {
const newSubscribed = new Set(this.subscribedIncidents);
if (newSubscribed.has(incident.id)) {
newSubscribed.delete(incident.id);
this.dispatchEvent(new CustomEvent('incidentUnsubscribe', {
detail: {
incident,
incidentId: incident.id
},
bubbles: true,
composed: true
}));
} else {
newSubscribed.add(incident.id);
this.dispatchEvent(new CustomEvent('incidentSubscribe', {
detail: {
incident,
incidentId: incident.id,
incidentTitle: incident.title,
affectedServices: incident.affectedServices
},
bubbles: true,
composed: true
}));
}
this.subscribedIncidents = newSubscribed;
}
public dispatchReportNewIncident() { public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', {})); this.dispatchEvent(new CustomEvent('reportNewIncident', {
bubbles: true,
composed: true
}));
} }
public dispatchStatusSubscribe() { public dispatchStatusSubscribe() {
this.dispatchEvent(new CustomEvent('statusSubscribe', {})); this.dispatchEvent(new CustomEvent('statusSubscribe', {
bubbles: true,
composed: true
}));
} }
} }

View File

@@ -0,0 +1,25 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
.demo-container {
background: #fafafa;
padding: 40px 0;
min-height: 400px;
}
</style>
<div class="demo-container">
<upl-statuspage-pagetitle
.pageTitle=${'System Status'}
.pageSubtitle=${'Real-time operational status and incident reports for all services'}
></upl-statuspage-pagetitle>
<br>
<upl-statuspage-pagetitle
.pageTitle=${'API Documentation'}
.pageSubtitle=${'Comprehensive guides and references for integrating with our platform'}
.centered=${true}
></upl-statuspage-pagetitle>
</div>
`;

View File

@@ -0,0 +1,89 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as sharedStyles from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-pagetitle.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upl-statuspage-pagetitle': UplStatuspagePagetitle;
}
}
@customElement('upl-statuspage-pagetitle')
export class UplStatuspagePagetitle extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor pageTitle: string = 'System Status';
@property({ type: String })
accessor pageSubtitle: string = '';
@property({ type: Boolean })
accessor centered: boolean = false;
constructor() {
super();
}
public static styles = [
domtools.elementBasic.staticStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.title-container {
max-width: 1200px;
margin: 0 auto;
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.title-container.centered {
text-align: center;
}
h1 {
font-size: 48px;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
color: ${sharedStyles.colors.text.primary};
margin: 0 0 ${unsafeCSS(sharedStyles.spacing.md)} 0;
}
p {
font-size: 18px;
color: ${sharedStyles.colors.text.secondary};
margin: 0;
line-height: 1.6;
}
@media (max-width: 640px) {
.title-container {
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.md)};
}
h1 {
font-size: 32px;
}
p {
font-size: 16px;
}
}
`
]
public render(): TemplateResult {
return html`
<div class="title-container ${this.centered ? 'centered' : ''}">
<h1>${this.pageTitle}</h1>
${this.pageSubtitle ? html`
<p>${this.pageSubtitle}</p>
` : ''}
</div>
`;
}
}

View File

@@ -0,0 +1,315 @@
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;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
</style>
<div class="demo-container">
<!-- Normal Operation -->
<div class="demo-section">
<div class="demo-title">Normal Operation</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
// Set initial values
statsGrid.currentStatus = 'operational';
statsGrid.uptime = 99.95;
statsGrid.avgResponseTime = 125;
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
statsGrid.totalServices = 12;
statsGrid.timePeriod = '90 days';
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
<!-- Degraded Performance -->
<div class="demo-section">
<div class="demo-title">Degraded Performance</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
statsGrid.currentStatus = 'degraded';
statsGrid.uptime = 98.50;
statsGrid.avgResponseTime = 450;
statsGrid.totalIncidents = 3;
statsGrid.affectedServices = 2;
statsGrid.totalServices = 12;
statsGrid.timePeriod = '30 days';
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
<!-- Major Outage -->
<div class="demo-section">
<div class="demo-title">Major Outage</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
statsGrid.currentStatus = 'major_outage';
statsGrid.uptime = 95.20;
statsGrid.avgResponseTime = 1250;
statsGrid.totalIncidents = 8;
statsGrid.affectedServices = 7;
statsGrid.totalServices = 12;
statsGrid.timePeriod = '7 days';
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
<!-- Interactive Demo -->
<div class="demo-section">
<div class="demo-title">Interactive Demo</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
// Initial state
statsGrid.currentStatus = 'operational';
statsGrid.uptime = 99.99;
statsGrid.avgResponseTime = 85;
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
statsGrid.totalServices = 15;
statsGrid.timePeriod = '90 days';
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
// Status buttons
const statuses = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
statuses.forEach((status, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statsGrid.currentStatus = status;
// Adjust other values based on status
switch (status) {
case 'operational':
statsGrid.uptime = 99.99;
statsGrid.avgResponseTime = 85;
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
break;
case 'degraded':
statsGrid.uptime = 98.50;
statsGrid.avgResponseTime = 350;
statsGrid.totalIncidents = 2;
statsGrid.affectedServices = 1;
break;
case 'partial_outage':
statsGrid.uptime = 97.00;
statsGrid.avgResponseTime = 750;
statsGrid.totalIncidents = 5;
statsGrid.affectedServices = 3;
break;
case 'major_outage':
statsGrid.uptime = 94.50;
statsGrid.avgResponseTime = 1500;
statsGrid.totalIncidents = 10;
statsGrid.affectedServices = 8;
break;
case 'maintenance':
statsGrid.uptime = 99.00;
statsGrid.avgResponseTime = 150;
statsGrid.totalIncidents = 1;
statsGrid.affectedServices = 2;
break;
}
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add time period selector
const timePeriodControls = document.createElement('div');
timePeriodControls.className = 'demo-controls';
timePeriodControls.style.marginTop = '10px';
const periods = ['24 hours', '7 days', '30 days', '90 days'];
periods.forEach((period, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 3 ? ' active' : '');
button.textContent = period;
button.onclick = () => {
timePeriodControls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statsGrid.timePeriod = period;
};
timePeriodControls.appendChild(button);
});
wrapperElement.appendChild(timePeriodControls);
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
<!-- Loading State -->
<div class="demo-section">
<div class="demo-title">Loading State</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
statsGrid.loading = true;
// Create toggle button
const controls = document.createElement('div');
controls.className = 'demo-controls';
const toggleButton = document.createElement('button');
toggleButton.className = 'demo-button';
toggleButton.textContent = 'Toggle Loading';
toggleButton.onclick = () => {
statsGrid.loading = !statsGrid.loading;
if (!statsGrid.loading) {
statsGrid.currentStatus = 'operational';
statsGrid.uptime = 99.95;
statsGrid.avgResponseTime = 125;
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
statsGrid.totalServices = 12;
}
};
controls.appendChild(toggleButton);
wrapperElement.appendChild(controls);
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
<!-- Real-time Updates -->
<div class="demo-section">
<div class="demo-title">Real-time Updates</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
// Initial values
statsGrid.currentStatus = 'operational';
statsGrid.uptime = 99.95;
statsGrid.avgResponseTime = 100;
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
statsGrid.totalServices = 10;
statsGrid.timePeriod = '24 hours';
// Simulate real-time updates
let interval = setInterval(() => {
// Slight variations in response time
statsGrid.avgResponseTime = Math.floor(80 + Math.random() * 40);
// Occasionally change status
if (Math.random() < 0.1) {
const statuses = ['operational', 'degraded'];
statsGrid.currentStatus = statuses[Math.floor(Math.random() * statuses.length)];
if (statsGrid.currentStatus === 'degraded') {
statsGrid.avgResponseTime = Math.floor(300 + Math.random() * 200);
statsGrid.totalIncidents = Math.min(statsGrid.totalIncidents + 1, 5);
statsGrid.affectedServices = Math.min(Math.floor(Math.random() * 3) + 1, statsGrid.totalServices);
statsGrid.uptime = Math.max(99.0, statsGrid.uptime - 0.05);
} else {
statsGrid.affectedServices = 0;
}
}
}, 2000);
// Add control button
const controls = document.createElement('div');
controls.className = 'demo-controls';
const toggleButton = document.createElement('button');
toggleButton.className = 'demo-button active';
toggleButton.textContent = 'Stop Updates';
toggleButton.onclick = () => {
if (interval) {
clearInterval(interval);
interval = null;
toggleButton.textContent = 'Start Updates';
toggleButton.classList.remove('active');
} else {
interval = setInterval(() => {
statsGrid.avgResponseTime = Math.floor(80 + Math.random() * 40);
if (Math.random() < 0.1) {
const statuses = ['operational', 'degraded'];
statsGrid.currentStatus = statuses[Math.floor(Math.random() * statuses.length)];
}
}, 2000);
toggleButton.textContent = 'Stop Updates';
toggleButton.classList.add('active');
}
};
controls.appendChild(toggleButton);
wrapperElement.appendChild(controls);
// Cleanup on unmount
wrapperElement.addEventListener('remove', () => {
if (interval) clearInterval(interval);
});
}}
>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -0,0 +1,478 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statsgrid.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upl-statuspage-statsgrid': UplStatuspageStatsgrid;
}
}
@customElement('upl-statuspage-statsgrid')
export class UplStatuspageStatsgrid extends DeesElement {
public static demo = demoFunc;
@property({ type: Number })
accessor uptime: number = 99.99;
@property({ type: Number })
accessor avgResponseTime: number = 125;
@property({ type: Number })
accessor totalIncidents: number = 0;
@property({ type: Number })
accessor affectedServices: number = 0;
@property({ type: Number })
accessor totalServices: number = 0;
@property({ type: String })
accessor currentStatus: string = 'operational';
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: String })
accessor timePeriod: string = '90 days';
constructor() {
super();
}
public static styles = [
domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css`
:host {
display: block;
background: transparent;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.stat-card {
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
overflow: hidden;
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.stat-card:nth-child(1) { animation-delay: 0ms; }
.stat-card:nth-child(2) { animation-delay: 50ms; }
.stat-card:nth-child(3) { animation-delay: 100ms; }
.stat-card:nth-child(4) { animation-delay: 150ms; }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Status-colored top accent */
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: ${sharedStyles.colors.border.muted};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.stat-card.status-card::before {
background: ${sharedStyles.colors.status.operational};
}
.stat-card.status-card.degraded::before {
background: ${sharedStyles.colors.status.degraded};
}
.stat-card.status-card.partial_outage::before {
background: ${sharedStyles.colors.status.partial};
}
.stat-card.status-card.major_outage::before {
background: ${sharedStyles.colors.status.major};
}
.stat-card.status-card.maintenance::before {
background: ${sharedStyles.colors.status.maintenance};
}
.stat-card.uptime-card::before {
background: ${sharedStyles.colors.status.operational};
}
.stat-card.response-card::before {
background: ${sharedStyles.colors.status.maintenance};
}
.stat-card.incident-card::before {
background: ${sharedStyles.colors.status.partial};
}
.stat-card:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
transform: translateY(-3px);
}
.stat-card:hover::before {
height: 4px;
}
.stat-label {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
display: flex;
align-items: center;
gap: 6px;
}
.stat-label svg {
width: 14px;
height: 14px;
opacity: 0.7;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: ${sharedStyles.colors.text.primary};
font-variant-numeric: tabular-nums;
line-height: 1.2;
letter-spacing: -0.02em;
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.stat-card:hover .stat-value {
transform: scale(1.02);
}
.stat-unit {
font-size: 16px;
font-weight: 500;
color: ${sharedStyles.colors.text.secondary};
margin-left: 2px;
}
.stat-change {
font-size: 12px;
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
width: fit-content;
}
.stat-change.positive {
color: ${sharedStyles.colors.status.operational};
background: ${cssManager.bdTheme('rgba(22, 163, 74, 0.08)', 'rgba(34, 197, 94, 0.12)')};
}
.stat-change.negative {
color: ${sharedStyles.colors.status.partial};
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.08)', 'rgba(248, 113, 113, 0.12)')};
}
.stat-change.neutral {
color: ${sharedStyles.colors.text.muted};
background: ${sharedStyles.colors.background.muted};
}
/* Status text color variations */
.stat-value.operational {
color: ${sharedStyles.colors.status.operational};
}
.stat-value.degraded {
color: ${sharedStyles.colors.status.degraded};
}
.stat-value.partial_outage {
color: ${sharedStyles.colors.status.partial};
}
.stat-value.major_outage {
color: ${sharedStyles.colors.status.major};
}
.stat-value.maintenance {
color: ${sharedStyles.colors.status.maintenance};
}
.loading-skeleton {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.skeleton-card {
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
height: 110px;
position: relative;
overflow: hidden;
}
.skeleton-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: ${sharedStyles.colors.background.muted};
}
.skeleton-label {
height: 12px;
width: 80px;
background: ${sharedStyles.colors.background.muted};
border-radius: 4px;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
animation: shimmer 1.5s infinite;
}
.skeleton-value {
height: 32px;
width: 100px;
background: ${sharedStyles.colors.background.muted};
border-radius: 6px;
animation: shimmer 1.5s infinite;
animation-delay: 0.1s;
}
.skeleton-change {
height: 20px;
width: 70px;
background: ${sharedStyles.colors.background.muted};
border-radius: 4px;
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
animation: shimmer 1.5s infinite;
animation-delay: 0.2s;
}
@keyframes shimmer {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.operational {
background: ${sharedStyles.colors.status.operational};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(22, 163, 74, 0.2)', 'rgba(34, 197, 94, 0.25)')};
animation: statusPulse 2s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(22, 163, 74, 0.2)', 'rgba(34, 197, 94, 0.25)')}; }
50% { box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(22, 163, 74, 0.1)', 'rgba(34, 197, 94, 0.15)')}; }
}
.status-indicator.degraded {
background: ${sharedStyles.colors.status.degraded};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(217, 119, 6, 0.2)', 'rgba(251, 191, 36, 0.25)')};
}
.status-indicator.partial_outage {
background: ${sharedStyles.colors.status.partial};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(239, 68, 68, 0.2)', 'rgba(248, 113, 113, 0.25)')};
}
.status-indicator.major_outage {
background: ${sharedStyles.colors.status.major};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(185, 28, 28, 0.2)', 'rgba(239, 68, 68, 0.25)')};
animation: majorPulse 1s ease-in-out infinite;
}
@keyframes majorPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-indicator.maintenance {
background: ${sharedStyles.colors.status.maintenance};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(37, 99, 235, 0.2)', 'rgba(96, 165, 250, 0.25)')};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.stat-card {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
}
.stat-value {
font-size: 22px;
}
.stat-label {
font-size: 10px;
}
.stat-label svg {
width: 12px;
height: 12px;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="container">
${this.loading ? html`
<div class="loading-skeleton">
${Array(4).fill(0).map(() => html`
<div class="skeleton-card">
<div class="skeleton-label"></div>
<div class="skeleton-value"></div>
</div>
`)}
</div>
` : html`
<div class="stats-grid">
<div class="stat-card status-card ${this.currentStatus}">
<div class="stat-label">
<span class="status-indicator ${this.currentStatus}"></span>
Current Status
</div>
<div class="stat-value ${this.currentStatus}">
${this.formatStatus(this.currentStatus)}
</div>
</div>
<div class="stat-card uptime-card">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
Uptime
</div>
<div class="stat-value">
${this.uptime.toFixed(2)}<span class="stat-unit">%</span>
</div>
<div class="stat-change neutral">
Last ${this.timePeriod}
</div>
</div>
<div class="stat-card response-card">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
Avg Response
</div>
<div class="stat-value">
${this.avgResponseTime}<span class="stat-unit">ms</span>
</div>
${this.renderResponseChange()}
</div>
<div class="stat-card incident-card">
<div class="stat-label">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
Incidents
</div>
<div class="stat-value">
${this.totalIncidents}
</div>
<div class="stat-change neutral">
${this.affectedServices} of ${this.totalServices} services
</div>
</div>
</div>
`}
</div>
`;
}
private formatStatus(status: string): string {
const statusMap: Record<string, string> = {
operational: 'Operational',
degraded: 'Degraded',
partial_outage: 'Partial Outage',
major_outage: 'Major Outage',
maintenance: 'Maintenance',
};
return statusMap[status] || 'Unknown';
}
private renderResponseChange(): TemplateResult {
// This could be enhanced with actual trend data
const trend = this.avgResponseTime < 200 ? 'positive' : this.avgResponseTime > 500 ? 'negative' : 'neutral';
const icon = trend === 'positive' ? '↓' : trend === 'negative' ? '↑' : '→';
return html`
<div class="stat-change ${trend}">
<span>${icon}</span>
<span>${trend === 'positive' ? 'Fast' : trend === 'negative' ? 'Slow' : 'Normal'}</span>
</div>
`;
}
}

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, TemplateResult, cssManager, css } from '@designestate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@designestate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import type { IOverallStatus } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-statusbar.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -9,9 +12,22 @@ declare global {
@customElement('upl-statuspage-statusbar') @customElement('upl-statuspage-statusbar')
export class UplStatuspageStatusbar extends DeesElement { export class UplStatuspageStatusbar extends DeesElement {
public static demo = () => html` public static demo = demoFunc;
<upl-statuspage-statusbar></upl-statuspage-statusbar>
`; @property({ type: Object })
accessor overallStatus: IOverallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 0
};
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Boolean })
accessor expandable: boolean = true;
constructor() { constructor() {
super(); super();
@@ -21,30 +37,326 @@ export class UplStatuspageStatusbar extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
padding: 20px 0px 15px 0px; padding: 0;
display: block; display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};; background: transparent;
font-family: Inter; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: #fff; }
.statusbar-container {
margin: auto;
max-width: 1200px;
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
position: relative;
}
.statusbar-inner {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 72px;
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.xl)};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.xl)};
cursor: default;
transition: all ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
overflow: hidden;
font-weight: 500;
font-size: 15px;
letter-spacing: -0.01em;
border: 1px solid ${sharedStyles.colors.border.default};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
/* Gradient background overlay */
.statusbar-inner::before {
content: '';
position: absolute;
inset: 0;
opacity: 1;
transition: opacity ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
z-index: 0;
}
/* Left accent border */
.statusbar-inner::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
transition: background ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
z-index: 1;
}
/* Operational - green gradient */
.statusbar-inner.operational {
background: ${sharedStyles.colors.background.card};
}
.statusbar-inner.operational::before {
background: ${sharedStyles.statusGradients.operational};
}
.statusbar-inner.operational::after {
background: ${sharedStyles.colors.status.operational};
}
/* Degraded - yellow/amber gradient */
.statusbar-inner.degraded {
background: ${sharedStyles.colors.background.card};
}
.statusbar-inner.degraded::before {
background: ${sharedStyles.statusGradients.degraded};
}
.statusbar-inner.degraded::after {
background: ${sharedStyles.colors.status.degraded};
}
/* Partial outage - orange/red gradient */
.statusbar-inner.partial_outage {
background: ${sharedStyles.colors.background.card};
}
.statusbar-inner.partial_outage::before {
background: ${sharedStyles.statusGradients.partial};
}
.statusbar-inner.partial_outage::after {
background: ${sharedStyles.colors.status.partial};
}
/* Major outage - red gradient */
.statusbar-inner.major_outage {
background: ${sharedStyles.colors.background.card};
}
.statusbar-inner.major_outage::before {
background: ${sharedStyles.statusGradients.major};
}
.statusbar-inner.major_outage::after {
background: ${sharedStyles.colors.status.major};
}
/* Maintenance - blue gradient */
.statusbar-inner.maintenance {
background: ${sharedStyles.colors.background.card};
}
.statusbar-inner.maintenance::before {
background: ${sharedStyles.statusGradients.maintenance};
}
.statusbar-inner.maintenance::after {
background: ${sharedStyles.colors.status.maintenance};
}
.statusbar-inner:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
transform: translateY(-1px);
}
.statusbar-inner:hover::before {
opacity: 1.2;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
position: relative;
}
.status-indicator::after {
content: '';
position: absolute;
inset: -3px;
border-radius: 50%;
opacity: 0.2;
}
.statusbar-inner.operational .status-indicator {
background: ${sharedStyles.colors.status.operational};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(22, 163, 74, 0.15)', 'rgba(34, 197, 94, 0.2)')};
}
.statusbar-inner.operational .status-indicator::after {
background: ${sharedStyles.colors.status.operational};
animation: pulse-ring 2s ease-out infinite;
}
.statusbar-inner.degraded .status-indicator {
background: ${sharedStyles.colors.status.degraded};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(217, 119, 6, 0.15)', 'rgba(251, 191, 36, 0.2)')};
}
.statusbar-inner.partial_outage .status-indicator {
background: ${sharedStyles.colors.status.partial};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(220, 38, 38, 0.15)', 'rgba(248, 113, 113, 0.2)')};
}
.statusbar-inner.major_outage .status-indicator {
background: ${sharedStyles.colors.status.major};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(185, 28, 28, 0.15)', 'rgba(239, 68, 68, 0.2)')};
animation: pulse-indicator 1s ease-in-out infinite;
}
.statusbar-inner.maintenance .status-indicator {
background: ${sharedStyles.colors.status.maintenance};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(37, 99, 235, 0.15)', 'rgba(96, 165, 250, 0.2)')};
}
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 0.2; }
50% { transform: scale(1.5); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
@keyframes pulse-indicator {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-content {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
flex: 1;
padding-left: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.status-main {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
color: ${sharedStyles.colors.text.primary};
}
.status-message {
font-weight: 600;
}
.status-details {
font-size: 13px;
font-weight: 400;
color: ${sharedStyles.colors.text.secondary};
}
.loading-skeleton {
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
height: 64px;
position: relative;
overflow: hidden;
}
.loading-skeleton::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.04) 50%, transparent 100%)',
'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%)'
)};
animation: loading 1.5s ${unsafeCSS(sharedStyles.easings.default)} infinite;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
.last-updated {
font-size: 12px;
color: ${sharedStyles.colors.text.muted};
white-space: nowrap;
padding: 4px 10px;
background: ${sharedStyles.colors.background.muted};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.statusbar-inner:hover .last-updated {
background: ${cssManager.bdTheme('#e4e4e7', '#3f3f46')};
}
@media (max-width: 768px) {
.statusbar-container {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
} }
.mainbox { .statusbar-inner {
margin: auto; flex-direction: column;
max-width: 900px; align-items: flex-start;
text-align: center; gap: ${unsafeCSS(sharedStyles.spacing.sm)};
background: #19572E; min-height: auto;
line-height: 50px; padding: ${unsafeCSS(sharedStyles.spacing.md)};
border-radius: 3px;
} }
.status-main {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.last-updated {
align-self: flex-start;
}
}
@media (max-width: 640px) {
.statusbar-inner {
font-size: 14px;
}
.status-indicator {
width: 8px;
height: 8px;
}
.last-updated {
font-size: 11px;
}
}
`, `,
] ]
public render(): TemplateResult { public render(): TemplateResult {
const formatLastUpdated = () => {
const date = new Date(this.overallStatus.lastUpdated);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return date.toLocaleDateString();
};
return html` return html`
<style> <div class="statusbar-container">
</style> ${this.loading ? html`
<div class="mainbox"> <div class="loading-skeleton"></div>
Everything is working normally! ` : html`
<div class="statusbar-inner ${this.overallStatus.status}">
<div class="status-content">
<div class="status-indicator"></div>
<div class="status-main">
<span>${this.overallStatus.message}</span>
${this.overallStatus.affectedServices > 0 ? html`
<span class="status-details">
· ${this.overallStatus.affectedServices} of ${this.overallStatus.totalServices} services affected
</span>
` : ''}
</div>
</div>
<div class="last-updated">
${formatLastUpdated()}
</div>
</div>
`}
</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

@@ -4,12 +4,16 @@ import {
property, property,
html, html,
customElement, customElement,
TemplateResult, type TemplateResult,
css, css,
cssManager, cssManager,
} from '@designestate/dees-element'; unsafeCSS,
} from '@design.estate/dees-element';
import type { IStatusHistoryPoint } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js'; import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusdetails.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -19,7 +23,25 @@ declare global {
@customElement('upl-statuspage-statusdetails') @customElement('upl-statuspage-statusdetails')
export class UplStatuspageStatusdetails extends DeesElement { export class UplStatuspageStatusdetails extends DeesElement {
public static demo = () => html` <upl-statuspage-statusdetails></upl-statuspage-statusdetails> `; public static demo = demoFunc;
@property({ type: Array })
accessor historyData: IStatusHistoryPoint[] = [];
@property({ type: Array })
accessor dataPoints: IStatusHistoryPoint[] = [];
@property({ type: String })
accessor serviceId: string = '';
@property({ type: String })
accessor serviceName: string = 'Service';
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Number })
accessor hoursToShow: number = 48;
constructor() { constructor() {
super(); super();
@@ -27,69 +49,331 @@ export class UplStatuspageStatusdetails extends DeesElement {
public static styles = [ public static styles = [
plugins.domtools.elementBasic.staticStyles, plugins.domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css` css`
:host { :host {
position: relative; position: relative;
padding: 0px 0px 15px 0px;
display: block; display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};; background: transparent;
font-family: Inter; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: #fff; color: ${sharedStyles.colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.graph-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
}
.graph-container {
position: relative;
animation: fadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)};
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
} }
.mainbox { .mainbox {
margin: auto; background: transparent;
max-width: 900px; border: none;
text-align: right; border-radius: 0;
background: ${cssManager.bdTheme('#ffffff', '#333333')};; padding: 0;
line-height: 50px;
border-radius: 3px;
} }
.mainbox .barContainer { .mainbox .barContainer {
position: relative; position: relative;
display: flex; display: flex;
padding: 6px; gap: 2px;
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
overflow: hidden; overflow: hidden;
height: 40px;
} }
.mainbox .barContainer .bar { .mainbox .barContainer .bar {
margin: 4px; flex: 1;
width: 11px; height: 100%;
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
border-radius: 3px; border-radius: 3px;
height: 40px; animation: barGrow 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
background: #2deb51; animation-delay: calc(var(--bar-index, 0) * 8ms);
transform-origin: bottom;
} }
.timeIndicator {
@keyframes barGrow {
from {
transform: scaleY(0);
opacity: 0;
}
to {
transform: scaleY(1);
opacity: 1;
}
}
.mainbox .barContainer .bar:hover {
transform: scaleY(1.15);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
z-index: 1;
}
.mainbox .barContainer .bar.operational {
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
}
.mainbox .barContainer .bar.degraded {
background: ${cssManager.bdTheme('#fbbf24', '#fbbf24')};
}
.mainbox .barContainer .bar.partial_outage {
background: ${cssManager.bdTheme('#f87171', '#f87171')};
}
.mainbox .barContainer .bar.major_outage {
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.mainbox .barContainer .bar.maintenance {
background: ${cssManager.bdTheme('#60a5fa', '#60a5fa')};
}
.mainbox .barContainer .bar.no-data {
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
opacity: 0.6;
}
.time-labels {
display: flex;
justify-content: space-between;
padding: 0;
margin-top: ${unsafeCSS(sharedStyles.spacing.xs)};
font-size: 10px;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
opacity: 0.8;
}
.tooltip {
position: absolute; position: absolute;
width: 11px; background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
height: 11px; color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
background: #FF9800; padding: 8px 12px;
top: 56px; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
left: 400px; font-size: 11px;
transform: rotate(45deg); pointer-events: none;
opacity: 0;
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)},
transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
z-index: 50;
white-space: nowrap;
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
line-height: 1.5;
transform: translateY(4px);
}
.tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-time {
font-weight: 600;
display: block;
margin-bottom: 2px;
}
.tooltip-stat {
font-size: 10px;
opacity: 0.9;
}
.loading-skeleton {
display: flex;
gap: 1px;
height: 24px;
}
.loading-skeleton .skeleton-bar {
flex: 1;
height: 100%;
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.graph-container {
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.mainbox .barContainer {
height: 32px;
padding: ${unsafeCSS(sharedStyles.spacing.xs)};
}
.time-labels {
font-size: 9px;
}
.stats-row {
font-size: 11px;
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
align-items: flex-start;
}
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<style></style> <div class="container">
<uplinternal-miniheading>Yesterday & Today</uplinternal-miniheading> <uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
<div class="mainbox"> <div class="mainbox">
<div class="barContainer"> ${this.loading ? html`
${(() => { <div class="graph-container">
let counter = 0; <div class="barContainer" style="background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#f3f4f6', '#1f1f1f')}; border-radius: ${sharedStyles.borderRadius.base}; padding: ${sharedStyles.spacing.sm}; height: 40px;">
const returnArray: TemplateResult[] = []; <div class="loading-skeleton">
while (counter < 48) { ${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
counter++; </div>
returnArray.push(html` <div class="bar"></div> `); </div>
} </div>
return returnArray; ` : html`
})()} <div class="graph-container">
<div class="timeIndicator"></div> <div class="barContainer" @mouseleave=${this.hideTooltip}>
${this.renderBars()}
</div>
</div>
<div class="time-labels">
<span>${this.getTimeLabel(this.hoursToShow - 1)}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow * 3 / 4))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 2))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 4))}</span>
<span>now</span>
</div>
`}
</div> </div>
<div class="tooltip" id="tooltip"></div>
</div> </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}"
style="--bar-index: ${i}"
@mouseenter=${(e: MouseEvent) => this.showTooltip(e, timestamp, status, responseTime)}
@click=${() => this.handleBarClick(timestamp, status, responseTime)}
></div>
`);
}
return bars;
}
private getData(): IStatusHistoryPoint[] {
return this.dataPoints?.length > 0 ? this.dataPoints : this.historyData || [];
}
private findDataPointForTime(timestamp: number): IStatusHistoryPoint | undefined {
const data = this.getData();
if (!data || data.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 data.find(point => {
const pointDate = new Date(point.timestamp);
return pointDate.toDateString() === targetDate &&
pointDate.getHours() === targetHour;
});
}
private getTimeLabel(hoursAgo: number): string {
const date = new Date(Date.now() - (hoursAgo * 60 * 60 * 1000));
if (hoursAgo >= 24) {
return `${date.getMonth() + 1}/${date.getDate()}`;
}
return `${date.getHours()}h`;
}
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 class="tooltip-time">${timeStr}</div>
<div class="tooltip-stat">Status: ${statusStr}</div>
${responseTime > 0 ? `<div class="tooltip-stat">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 - 10}px`;
tooltip.style.transform = 'translate(-50%, -100%)';
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

@@ -3,13 +3,17 @@ import {
property, property,
html, html,
customElement, customElement,
TemplateResult, type TemplateResult,
css, css,
cssManager cssManager,
} from '@designestate/dees-element'; unsafeCSS
import * as domtools from '@designestate/dees-domtools'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IMonthlyUptime } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js'; import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusmonth.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -19,7 +23,25 @@ declare global {
@customElement('upl-statuspage-statusmonth') @customElement('upl-statuspage-statusmonth')
export class UplStatuspageStatusmonth extends DeesElement { export class UplStatuspageStatusmonth extends DeesElement {
public static demo = () => html` <upl-statuspage-statusmonth></upl-statuspage-statusmonth> `; public static demo = demoFunc;
@property({ type: Array })
accessor monthlyData: IMonthlyUptime[] = [];
@property({ type: String })
accessor serviceId: string = '';
@property({ type: String })
accessor serviceName: string = 'Service';
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Boolean })
accessor showTooltip: boolean = true;
@property({ type: Number })
accessor monthsToShow: number = 5;
constructor() { constructor() {
super(); super();
@@ -27,104 +49,587 @@ export class UplStatuspageStatusmonth extends DeesElement {
public static styles = [ public static styles = [
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css` css`
:host { :host {
position: relative; position: relative;
padding: 0px 0px 15px 0px; display: block;
display: block; background: transparent;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};; font-family: ${unsafeCSS(sharedStyles.fonts.base)};
font-family: Inter; color: ${sharedStyles.colors.text.primary};
color: #fff; }
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.mainbox {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
}
/* Month card with entrance animation */
.statusMonth {
background: ${sharedStyles.colors.background.card};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
border: 1px solid ${sharedStyles.colors.border.default};
position: relative;
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
display: flex;
flex-direction: column;
min-height: 280px;
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
animation: fadeInUp 0.5s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.statusMonth:nth-child(1) { animation-delay: 0ms; }
.statusMonth:nth-child(2) { animation-delay: 100ms; }
.statusMonth:nth-child(3) { animation-delay: 200ms; }
.statusMonth:nth-child(4) { animation-delay: 300ms; }
.statusMonth:nth-child(5) { animation-delay: 400ms; }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.statusMonth:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
transform: translateY(-2px);
}
.month-header {
font-size: 12px;
font-weight: 600;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
color: ${sharedStyles.colors.text.primary};
letter-spacing: 0.04em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 8px;
}
.month-header .current-badge {
font-size: 9px;
padding: 2px 6px;
background: ${sharedStyles.colors.status.operational};
color: white;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
font-weight: 500;
letter-spacing: 0.02em;
}
.days-container {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
width: 100%;
}
.weekday-label {
font-size: 9px;
text-align: center;
color: ${sharedStyles.colors.text.muted};
font-weight: 600;
height: 20px;
line-height: 20px;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
text-transform: uppercase;
letter-spacing: 0.02em;
}
/* Calendar day cell */
.statusDay {
aspect-ratio: 1;
border-radius: 4px;
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
position: relative;
animation: dayFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
animation-delay: calc(var(--day-index, 0) * 15ms);
}
@keyframes dayFadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.statusDay:hover:not(.empty) {
transform: scale(1.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
z-index: 2;
}
/* Current day highlight */
.statusDay.today {
box-shadow: 0 0 0 2px ${sharedStyles.colors.text.primary};
z-index: 1;
}
.statusDay.today:hover {
box-shadow: 0 0 0 2px ${sharedStyles.colors.text.primary}, 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Status colors with intensity variations based on uptime */
.statusDay.operational {
background: ${sharedStyles.colors.status.operational};
}
.statusDay.operational.uptime-high {
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
}
.statusDay.operational.uptime-mid {
background: ${cssManager.bdTheme('#4ade80', '#4ade80')};
}
.statusDay.degraded {
background: ${sharedStyles.colors.status.degraded};
}
.statusDay.partial_outage {
background: ${sharedStyles.colors.status.partial};
}
.statusDay.major_outage {
background: ${sharedStyles.colors.status.major};
animation: dayFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both,
majorOutagePulse 2s ease-in-out infinite;
}
@keyframes majorOutagePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.statusDay.maintenance {
background: ${sharedStyles.colors.status.maintenance};
}
.statusDay.no-data {
background: ${sharedStyles.colors.background.muted};
opacity: 0.5;
}
.statusDay.empty {
background: transparent;
cursor: default;
pointer-events: none;
animation: none;
}
/* Incident count indicator */
.incident-count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 8px;
font-weight: 700;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
line-height: 1;
}
/* Overall uptime footer */
.overall-uptime {
font-size: 12px;
margin-top: auto;
padding-top: ${unsafeCSS(sharedStyles.spacing.md)};
color: ${sharedStyles.colors.text.secondary};
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid ${sharedStyles.colors.border.default};
}
.uptime-stat {
display: flex;
justify-content: space-between;
align-items: center;
}
.uptime-value {
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
font-variant-numeric: tabular-nums;
font-size: 13px;
}
.uptime-value.good {
color: ${sharedStyles.colors.status.operational};
}
.uptime-value.warning {
color: ${sharedStyles.colors.status.degraded};
}
.uptime-value.bad {
color: ${sharedStyles.colors.status.partial};
}
/* Uptime bar visualization */
.uptime-bar {
height: 4px;
background: ${sharedStyles.colors.background.muted};
border-radius: 2px;
overflow: hidden;
margin-top: 4px;
}
.uptime-bar-fill {
height: 100%;
border-radius: 2px;
transition: width ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
}
.uptime-bar-fill.good {
background: ${sharedStyles.colors.status.operational};
}
.uptime-bar-fill.warning {
background: ${sharedStyles.colors.status.degraded};
}
.uptime-bar-fill.bad {
background: ${sharedStyles.colors.status.partial};
}
/* Loading skeleton */
.loading-skeleton {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
height: 100%;
}
.skeleton-header {
height: 20px;
width: 80px;
background: ${sharedStyles.colors.background.muted};
border-radius: 4px;
animation: shimmer 1.5s infinite;
}
.skeleton-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}
.skeleton-day {
background: ${sharedStyles.colors.background.muted};
border-radius: 3px;
animation: shimmer 1.5s infinite;
animation-delay: calc(var(--index) * 30ms);
}
@keyframes shimmer {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
/* Tooltip */
.tooltip {
position: absolute;
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 10px 14px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)},
transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
z-index: 50;
white-space: nowrap;
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
line-height: 1.5;
transform: translateX(-50%) translateY(4px);
}
.tooltip.visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.tooltip-date {
font-weight: 600;
margin-bottom: 6px;
font-size: 13px;
}
.tooltip-stat {
font-size: 11px;
opacity: 0.85;
display: flex;
align-items: center;
gap: 6px;
}
.tooltip-stat + .tooltip-stat {
margin-top: 2px;
}
.tooltip-uptime-bar {
height: 3px;
width: 60px;
background: rgba(128, 128, 128, 0.3);
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.tooltip-uptime-fill {
height: 100%;
border-radius: 2px;
}
.no-data-message {
grid-column: 1 / -1;
text-align: center;
padding: ${unsafeCSS(sharedStyles.spacing['2xl'])};
color: ${sharedStyles.colors.text.secondary};
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
} }
.mainbox { .mainbox {
margin: auto; grid-template-columns: 1fr;
max-width: 900px; gap: ${unsafeCSS(sharedStyles.spacing.md)};
display: grid;
grid-template-columns: repeat(5, calc(100% / 5 - 80px / 5));
grid-column-gap: 20px;
} }
.statusMonth { .statusMonth {
background: ${cssManager.bdTheme('#ffffff', '#333333')};; padding: ${unsafeCSS(sharedStyles.spacing.md)};
min-height: 20px; min-height: 260px;
display: grid;
padding: 10px;
grid-template-columns: repeat(6, auto);
grid-gap: 9px;
border-radius: 3px;
} }
.statusMonth .statusDay { .days-grid {
width: 16px; gap: 3px;
height: 16px;
background: #2deb51;
border-radius: 3px;
} }
.statusDay:hover:not(.empty) {
transform: scale(1.1);
}
.loading-skeleton {
height: 180px;
padding: ${unsafeCSS(sharedStyles.spacing.md)};
}
}
` `
] ]
public render(): TemplateResult { public render(): TemplateResult {
const totalDays = this.monthlyData.reduce((sum, month) => sum + month.days.length, 0);
return html` return html`
<style></style> <div class="container">
<uplinternal-miniheading>Last 150 days</uplinternal-miniheading> <uplinternal-miniheading>${this.serviceName} - Last ${totalDays} Days</uplinternal-miniheading>
<div class="mainbox"> <div class="mainbox">
<div class="statusMonth"> ${this.loading ? html`
${(() => { ${Array(this.monthsToShow).fill(0).map((_, index) => html`
let counter = 0; <div class="statusMonth">
const returnArray: TemplateResult[] = []; <div class="loading-skeleton">
while (counter < 30) { <div class="skeleton-header"></div>
counter++; <div class="days-container">
returnArray.push(html` <div class="statusDay"></div> `); <div class="skeleton-grid">
} ${Array(42).fill(0).map((_, i) => html`
return returnArray; <div class="skeleton-day" style="--index: ${i}"></div>
})()} `)}
</div>
</div>
<div style="height: 48px; border-top: 1px solid ${cssManager.bdTheme('#f3f4f6', '#1f1f1f')}; margin-top: auto; padding-top: 16px;"></div>
</div>
</div>
`)}
` : this.monthlyData.length === 0 ? html`
<div class="no-data-message">No uptime data available</div>
` : this.monthlyData.map(month => this.renderMonth(month))}
</div> </div>
<div class="statusMonth"> ${this.showTooltip ? html`<div class="tooltip" id="tooltip"></div>` : ''}
${(() => { </div>
let counter = 0; `;
const returnArray: TemplateResult[] = []; }
while (counter < 30) {
counter++; private renderMonth(monthData: IMonthlyUptime): TemplateResult {
returnArray.push(html` <div class="statusDay"></div> `); const monthDate = new Date(monthData.month + '-01');
} const monthName = monthDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
return returnArray; const firstDayOfWeek = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1).getDay();
})()} const now = new Date();
const isCurrentMonth = monthDate.getMonth() === now.getMonth() && monthDate.getFullYear() === now.getFullYear();
const uptimeClass = this.getUptimeClass(monthData.overallUptime);
return html`
<div class="statusMonth" @mouseleave=${this.hideTooltip}>
<div class="month-header">
${monthName}
${isCurrentMonth ? html`<span class="current-badge">Current</span>` : ''}
</div> </div>
<div class="statusMonth"> <div class="days-container">
${(() => { <div class="days-grid">
let counter = 0; ${this.renderWeekdayLabels()}
const returnArray: TemplateResult[] = []; ${this.renderEmptyDays(firstDayOfWeek)}
while (counter < 30) { ${monthData.days.map((day, index) => this.renderDay(day, index))}
counter++; ${this.renderTrailingEmptyDays(firstDayOfWeek + monthData.days.length)}
returnArray.push(html` <div class="statusDay"></div> `); </div>
}
return returnArray;
})()}
</div> </div>
<div class="statusMonth"> <div class="overall-uptime">
${(() => { <div class="uptime-stat">
let counter = 0; <span>Uptime</span>
const returnArray: TemplateResult[] = []; <span class="uptime-value ${uptimeClass}">${monthData.overallUptime.toFixed(2)}%</span>
while (counter < 30) { </div>
counter++; <div class="uptime-bar">
returnArray.push(html` <div class="statusDay"></div> `); <div class="uptime-bar-fill ${uptimeClass}" style="width: ${monthData.overallUptime}%"></div>
} </div>
return returnArray; ${monthData.totalIncidents > 0 ? html`
})()} <div class="uptime-stat">
</div> <span>Incidents</span>
<div class="statusMonth"> <span class="uptime-value">${monthData.totalIncidents}</span>
${(() => { </div>
let counter = 0; ` : ''}
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
</div> </div>
</div> </div>
`; `;
} }
private getUptimeClass(uptime: number): string {
if (uptime >= 99.9) return 'good';
if (uptime >= 99) return 'warning';
return 'bad';
}
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 renderTrailingEmptyDays(totalCells: number): TemplateResult[] {
const remainder = totalCells % 7;
const trailingCount = remainder === 0 ? 0 : 7 - remainder;
return Array(trailingCount).fill(0).map(() => html`<div class="statusDay empty"></div>`);
}
private renderDay(day: any, index: number = 0): TemplateResult {
const status = day.status || 'no-data';
const date = new Date(day.date);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
const uptimeIntensity = this.getUptimeIntensity(day.uptime);
return html`
<div
class="statusDay ${status} ${isToday ? 'today' : ''} ${status === 'operational' ? uptimeIntensity : ''}"
style="--day-index: ${index}"
@mouseenter=${(e: MouseEvent) => this.showTooltip && this.showDayTooltip(e, day)}
@click=${() => this.handleDayClick(day)}
>
${(status === 'major_outage' || status === 'partial_outage') && day.incidents > 0 ? html`
<span class="incident-count">${day.incidents}</span>
` : ''}
</div>
`;
}
private getUptimeIntensity(uptime: number): string {
if (uptime >= 99.9) return 'uptime-high';
if (uptime >= 99) return 'uptime-mid';
return '';
}
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: 'short',
month: 'short',
day: 'numeric'
});
let statusText = day.status.replace(/_/g, ' ');
statusText = statusText.charAt(0).toUpperCase() + statusText.slice(1);
const uptimeColor = day.uptime >= 99.9 ? '#22c55e' :
day.uptime >= 99 ? '#fbbf24' : '#f87171';
tooltip.innerHTML = `
<div class="tooltip-date">${dateStr}</div>
<div class="tooltip-stat">
<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${uptimeColor};"></span>
${statusText}
</div>
<div class="tooltip-stat">Uptime: <strong>${day.uptime.toFixed(2)}%</strong></div>
${day.incidents > 0 ? `<div class="tooltip-stat">Incidents: <strong>${day.incidents}</strong></div>` : ''}
${day.totalDowntime > 0 ? `<div class="tooltip-stat">Downtime: <strong>${day.totalDowntime}m</strong></div>` : ''}
<div class="tooltip-uptime-bar">
<div class="tooltip-uptime-fill" style="width: ${day.uptime}%; background: ${uptimeColor};"></div>
</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 - 10}px`;
tooltip.style.transform = 'translateX(-50%) translateY(-100%)';
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

4
ts_web/pages/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './statuspage-demo.js';
export * from './statuspage-allgreen.js';
export * from './statuspage-outage.js';
export * from './statuspage-maintenance.js';

View File

@@ -0,0 +1,412 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
export const statuspageAllgreen = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.demo-page-wrapper > dees-demowrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configure Header
header.pageTitle = 'TechVault';
header.showReportButton = true;
header.showSubscribeButton = true;
header.logoUrl = 'https://via.placeholder.com/150x50/4CAF50/ffffff?text=TV';
// Configure Overall Status - All Green
statusBar.overallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 12
};
// Configure Services - All Operational
const services: IServiceStatus[] = [
{
id: 'web-app',
name: 'web-app',
displayName: 'Web Application',
description: 'Main customer-facing application',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.98,
responseTime: 32,
category: 'Frontend',
selected: true
},
{
id: 'mobile-app',
name: 'mobile-app',
displayName: 'Mobile Applications',
description: 'iOS and Android apps',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 45,
category: 'Frontend',
selected: true
},
{
id: 'api',
name: 'api',
displayName: 'API Services',
description: 'RESTful and GraphQL APIs',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.97,
uptime90d: 99.96,
responseTime: 28,
category: 'Backend',
selected: true
},
{
id: 'auth',
name: 'auth',
displayName: 'Authentication',
description: 'OAuth and SSO services',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 22,
category: 'Backend',
selected: true
},
{
id: 'database',
name: 'database',
displayName: 'Database Cluster',
description: 'Primary data storage',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 15,
category: 'Infrastructure',
selected: true
},
{
id: 'cache',
name: 'cache',
displayName: 'Cache Layer',
description: 'Redis cache clusters',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 3,
category: 'Infrastructure',
selected: false
},
{
id: 'search',
name: 'search',
displayName: 'Search Service',
description: 'Elasticsearch clusters',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.97,
responseTime: 42,
category: 'Infrastructure',
selected: true
},
{
id: 'cdn',
name: 'cdn',
displayName: 'CDN',
description: 'Global content delivery',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 8,
category: 'Network',
selected: true
},
{
id: 'email',
name: 'email',
displayName: 'Email Service',
description: 'Transactional emails',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.94,
responseTime: 125,
category: 'Communication',
selected: false
},
{
id: 'sms',
name: 'sms',
displayName: 'SMS Service',
description: 'SMS notifications',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.92,
uptime90d: 99.90,
responseTime: 180,
category: 'Communication',
selected: false
},
{
id: 'payments',
name: 'payments',
displayName: 'Payment Processing',
description: 'Payment gateway integration',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 95,
category: 'Services',
selected: true
},
{
id: 'analytics',
name: 'analytics',
displayName: 'Analytics Engine',
description: 'Real-time analytics',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.96,
uptime90d: 99.95,
responseTime: 55,
category: 'Services',
selected: true
}
];
assetsSelector.services = services;
// Configure Stats Grid - All green metrics
statsGrid.currentStatus = 'operational';
statsGrid.uptime = 99.98;
statsGrid.avgResponseTime = Math.round(services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length);
statsGrid.totalIncidents = 0;
statsGrid.affectedServices = 0;
statsGrid.totalServices = services.length;
statsGrid.timePeriod = '30 days';
// Configure Status Details - All operational for 48 hours
const generateStatusDetails = (): IStatusHistoryPoint[] => {
const details: IStatusHistoryPoint[] = [];
const now = new Date();
for (let i = 47; i >= 0; i--) {
const date = new Date(now);
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
details.push({
timestamp: date.getTime(),
status: 'operational',
responseTime: 20 + Math.random() * 10 // Very consistent response times
});
}
return details;
};
statusDetails.dataPoints = generateStatusDetails();
statusDetails.serviceId = 'api';
statusDetails.serviceName = 'API Services';
// Configure Monthly Status - Near perfect uptime
const generateMonthlyData = (): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let m = 4; m >= 0; m--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
const monthKey = monthDate.toISOString().slice(0, 7);
const days = [];
let totalUptime = 0;
let incidents = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
const isFuture = dayDate > now;
if (isFuture) continue;
// Almost all days are perfect
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
let uptime = 100;
let dayIncidents = 0;
// Very rare minor issues
if (Math.random() > 0.98) {
status = 'degraded';
uptime = 99.5 + Math.random() * 0.5;
dayIncidents = 1;
}
days.push({
date: dayDate.toISOString().slice(0, 10),
status,
uptime,
incidents: dayIncidents,
totalDowntime: Math.round((100 - uptime) * 14.4)
});
totalUptime += uptime;
incidents += dayIncidents;
}
months.push({
month: monthKey,
days,
overallUptime: totalUptime / days.length,
totalIncidents: incidents
});
}
return months;
};
statusMonth.monthlyData = generateMonthlyData();
statusMonth.serviceId = 'all-services';
statusMonth.serviceName = 'All Services';
// Configure Incidents - None current, few past
const currentIncidents: IIncidentDetails[] = [];
const pastIncidents: IIncidentDetails[] = [
{
id: 'inc-2024-001',
title: 'Brief API Response Time Increase',
impact: 'API responses were slightly slower than usual',
severity: 'minor',
status: 'resolved',
startTime: Date.now() - 45 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 45 * 24 * 60 * 60 * 1000 + 15 * 60 * 1000,
affectedServices: ['API Services'],
rootCause: 'Temporary increase in traffic due to a viral marketing campaign.',
resolution: 'Auto-scaling handled the load increase. No manual intervention required.',
updates: [
{
id: 'update-1',
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Monitoring increased API response times.',
author: 'Automated Monitoring'
},
{
id: 'update-2',
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000,
status: 'identified',
message: 'Traffic spike identified. Auto-scaling in progress.',
author: 'DevOps Team'
},
{
id: 'update-3',
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000 + 15 * 60 * 1000,
status: 'resolved',
message: 'Response times back to normal. Incident resolved.',
author: 'Automated Monitoring'
}
]
},
{
id: 'inc-2023-089',
title: 'Scheduled Database Maintenance',
impact: 'Database was in read-only mode for 30 minutes',
severity: 'minor',
status: 'resolved',
startTime: Date.now() - 90 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 90 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
affectedServices: ['Database Cluster'],
rootCause: 'Scheduled maintenance for security patches.',
resolution: 'Maintenance completed successfully as planned.',
updates: [
{
id: 'update-4',
timestamp: Date.now() - 97 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Scheduled maintenance announced.',
author: 'Database Team'
},
{
id: 'update-5',
timestamp: Date.now() - 90 * 24 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Maintenance started. Database in read-only mode.',
author: 'Database Team'
},
{
id: 'update-6',
timestamp: Date.now() - 90 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
status: 'resolved',
message: 'Maintenance completed. All systems operational.',
author: 'Database Team'
}
]
}
];
incidents.currentIncidents = currentIncidents;
incidents.pastIncidents = pastIncidents;
// Configure Footer
footer.companyName = 'TechVault Services';
footer.legalUrl = 'https://techvault.example.com/legal';
footer.supportEmail = 'support@techvault.example.com';
footer.statusPageUrl = 'https://status.techvault.example.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/techvault' },
{ platform: 'github', url: 'https://github.com/techvault' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/techvault' },
{ platform: 'youtube', url: 'https://youtube.com/techvault' }
];
footer.rssFeedUrl = 'https://status.techvault.example.com/rss';
footer.apiStatusUrl = 'https://api.techvault.example.com/v1/status';
footer.enableSubscribe = true;
footer.subscriberCount = 5421;
footer.additionalLinks = [
{ label: 'API Documentation', url: 'https://docs.techvault.example.com' },
{ label: 'System Architecture', url: 'https://techvault.example.com/architecture' },
{ label: 'Security', url: 'https://techvault.example.com/security' }
];
}}
>
<upl-statuspage-header></upl-statuspage-header>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
<upl-statuspage-incidents></upl-statuspage-incidents>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
`;

View File

@@ -0,0 +1,653 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
export const statuspageDemo = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.demo-page-wrapper > dees-demowrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
const pageTitle = wrapperElement.querySelector('upl-statuspage-pagetitle') as any;
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configure Header
header.pageTitle = 'CloudFlow';
header.showReportButton = true;
header.showSubscribeButton = true;
header.logoUrl = 'https://via.placeholder.com/150x50/2196F3/ffffff?text=CF';
// Configure Page Title
pageTitle.pageTitle = 'Service Status';
pageTitle.pageSubtitle = 'Current operational status of CloudFlow Infrastructure services';
// Configure Overall Status
statusBar.overallStatus = {
status: 'degraded',
message: 'Minor service degradation in EU-West region',
lastUpdated: Date.now(),
affectedServices: 3,
totalServices: 18
};
// Configure Services
const services: IServiceStatus[] = [
// Core 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: 'Core Services',
selected: true
},
{
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: 32,
category: 'Core Services',
selected: true
},
{
id: 'user-dashboard',
name: 'user-dashboard',
displayName: 'User Dashboard',
description: 'Web application dashboard',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.97,
responseTime: 128,
category: 'Web Services',
selected: true
},
// Regional Services - US
{
id: 'us-east-compute',
name: 'us-east-compute',
displayName: 'US-East Compute',
description: 'Virtual machine instances',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.94,
uptime90d: 99.91,
responseTime: 22,
category: 'US-East',
selected: true
},
{
id: 'us-east-storage',
name: 'us-east-storage',
displayName: 'US-East Storage',
description: 'Object storage service',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 18,
category: 'US-East',
selected: true
},
{
id: 'us-east-database',
name: 'us-east-database',
displayName: 'US-East Database',
description: 'Managed database clusters',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.97,
uptime90d: 99.95,
responseTime: 14,
category: 'US-East',
selected: false
},
// Regional Services - EU (Degraded)
{
id: 'eu-west-compute',
name: 'eu-west-compute',
displayName: 'EU-West Compute',
description: 'Virtual machine instances',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 98.2,
uptime90d: 99.1,
responseTime: 145,
category: 'EU-West',
selected: true
},
{
id: 'eu-west-storage',
name: 'eu-west-storage',
displayName: 'EU-West Storage',
description: 'Object storage service',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 98.5,
uptime90d: 99.3,
responseTime: 220,
category: 'EU-West',
selected: true
},
{
id: 'eu-west-database',
name: 'eu-west-database',
displayName: 'EU-West Database',
description: 'Managed database clusters',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 97.8,
uptime90d: 98.9,
responseTime: 450,
category: 'EU-West',
selected: true
},
// Regional Services - Asia
{
id: 'ap-south-compute',
name: 'ap-south-compute',
displayName: 'Asia-Pacific Compute',
description: 'Virtual machine instances',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.91,
uptime90d: 99.88,
responseTime: 38,
category: 'Asia-Pacific',
selected: false
},
{
id: 'ap-south-storage',
name: 'ap-south-storage',
displayName: 'Asia-Pacific Storage',
description: 'Object storage service',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.93,
responseTime: 42,
category: 'Asia-Pacific',
selected: false
},
// Supporting Services
{
id: 'cdn',
name: 'cdn',
displayName: 'Content Delivery Network',
description: 'Global CDN for static assets',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 12,
category: 'Network',
selected: true
},
{
id: 'dns',
name: 'dns',
displayName: 'DNS Service',
description: 'Managed DNS resolution',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 8,
category: 'Network',
selected: false
},
{
id: 'monitoring',
name: 'monitoring',
displayName: 'Monitoring Service',
description: 'Infrastructure monitoring',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.92,
uptime90d: 99.90,
responseTime: 65,
category: 'Operations',
selected: true
},
{
id: 'logging',
name: 'logging',
displayName: 'Logging Service',
description: 'Centralized log management',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.88,
uptime90d: 99.85,
responseTime: 72,
category: 'Operations',
selected: false
},
{
id: 'backup-service',
name: 'backup-service',
displayName: 'Backup Service',
description: 'Automated backup system',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.98,
responseTime: 95,
category: 'Operations',
selected: false
},
{
id: 'email-service',
name: 'email-service',
displayName: 'Email Delivery',
description: 'Transactional email service',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.94,
uptime90d: 99.91,
responseTime: 125,
category: 'Communication',
selected: true
},
{
id: 'sms-service',
name: 'sms-service',
displayName: 'SMS Gateway',
description: 'SMS notification service',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.89,
uptime90d: 99.87,
responseTime: 180,
category: 'Communication',
selected: false
}
];
assetsSelector.services = services;
// Configure Status Details (48 hours of data)
const generateStatusDetails = (): IStatusHistoryPoint[] => {
const details: IStatusHistoryPoint[] = [];
const now = new Date();
for (let i = 47; i >= 0; i--) {
const date = new Date(now);
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
let status: 'operational' | 'degraded' | 'outage' = 'operational';
let value = 100;
// Simulate the EU-West issues from 6-3 hours ago
if (i >= 3 && i <= 6) {
status = 'degraded';
value = 85 + Math.random() * 10;
} else if (i > 6 && i <= 12) {
// Some minor issues earlier
if (Math.random() > 0.8) {
status = 'degraded';
value = 90 + Math.random() * 10;
}
}
details.push({
timestamp: date.getTime(),
status,
responseTime: status === 'operational' ? 20 + Math.random() * 30 : 50 + Math.random() * 100
});
}
return details;
};
statusDetails.dataPoints = generateStatusDetails();
statusDetails.serviceId = 'eu-west-database';
statusDetails.serviceName = 'EU-West Database';
// Configure Monthly Status
const generateMonthlyData = (): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let m = 4; m >= 0; m--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
const monthKey = monthDate.toISOString().slice(0, 7);
const days = [];
let totalUptime = 0;
let incidents = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
const isToday = dayDate.toDateString() === now.toDateString();
const isFuture = dayDate > now;
if (isFuture) continue;
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
let uptime = 100;
let dayIncidents = 0;
// Add some random incidents
if (!isToday && Math.random() > 0.92) {
status = 'degraded';
uptime = 95 + Math.random() * 4;
dayIncidents = 1;
} else if (!isToday && Math.random() > 0.98) {
status = 'partial_outage';
uptime = 80 + Math.random() * 15;
dayIncidents = 2;
}
// Today's incident
if (isToday) {
status = 'degraded';
uptime = 96.5;
dayIncidents = 1;
}
days.push({
date: dayDate.toISOString().slice(0, 10),
status,
uptime,
incidents: dayIncidents,
totalDowntime: Math.round((100 - uptime) * 14.4) // Convert to minutes (1440 minutes per day)
});
totalUptime += uptime;
incidents += dayIncidents;
}
months.push({
month: monthKey,
days,
overallUptime: totalUptime / days.length,
totalIncidents: incidents
});
}
return months;
};
statusMonth.monthlyData = generateMonthlyData();
statusMonth.serviceId = 'all-services';
statusMonth.serviceName = 'All Services';
// Configure Incidents
const currentIncidents: IIncidentDetails[] = [
{
id: 'inc-2024-001',
title: 'EU-West Database Performance Degradation',
impact: 'Users in EU-West region may experience slower database queries and occasional timeouts',
severity: 'major',
status: 'investigating',
startTime: Date.now() - 3 * 60 * 60 * 1000, // 3 hours ago
affectedServices: ['EU-West Database', 'EU-West Compute', 'EU-West Storage'],
updates: [
{
id: 'update-1',
timestamp: Date.now() - 3 * 60 * 60 * 1000,
status: 'investigating',
message: 'We are investigating reports of database performance issues in the EU-West region.',
author: 'CloudFlow Operations'
},
{
id: 'update-2',
timestamp: Date.now() - 2.5 * 60 * 60 * 1000,
status: 'identified',
message: 'We have identified unusual load patterns on our EU-West database cluster. Our team is working on redistributing the load.',
author: 'Database Team'
},
{
id: 'update-3',
timestamp: Date.now() - 1 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Load balancing adjustments have been applied. We are monitoring the situation closely. Performance is gradually improving.',
author: 'CloudFlow Operations'
}
]
}
];
const pastIncidents: IIncidentDetails[] = [
{
id: 'inc-2023-098',
title: 'Global Authentication Service Outage',
impact: 'Users were unable to log in to their accounts',
severity: 'critical',
status: 'resolved',
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000, // 2 hour duration
affectedServices: ['Authentication Service', 'User Dashboard', 'API Gateway'],
rootCause: 'A configuration change in our authentication service caused a cascading failure in the token validation system.',
resolution: 'The configuration was rolled back and additional safeguards were implemented to prevent similar issues.',
updates: [
{
id: 'update-4',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Multiple reports of login failures across all regions.',
author: 'CloudFlow Operations'
},
{
id: 'update-5',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
status: 'identified',
message: 'Root cause identified as misconfiguration in auth service. Rolling back changes.',
author: 'Security Team'
},
{
id: 'update-6',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 90 * 60 * 1000,
status: 'monitoring',
message: 'Service restored. Monitoring for any lingering issues.',
author: 'CloudFlow Operations'
},
{
id: 'update-7',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
status: 'resolved',
message: 'All systems operating normally. Incident resolved.',
author: 'CloudFlow Operations'
},
{
id: 'update-8',
timestamp: Date.now() - 6 * 24 * 60 * 60 * 1000,
status: 'postmortem',
message: 'Postmortem completed. Action items identified and assigned to prevent recurrence.',
author: 'Engineering Lead'
}
]
},
{
id: 'inc-2023-095',
title: 'US-East Storage Service Maintenance',
impact: 'Reduced redundancy for stored objects during maintenance window',
severity: 'minor',
status: 'resolved',
startTime: Date.now() - 14 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 14 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
affectedServices: ['US-East Storage'],
rootCause: 'Scheduled maintenance for storage infrastructure upgrade.',
resolution: 'Maintenance completed successfully. All systems operating at full redundancy.',
updates: [
{
id: 'update-9',
timestamp: Date.now() - 21 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Scheduled maintenance announced for US-East Storage service.',
author: 'CloudFlow Operations'
},
{
id: 'update-10',
timestamp: Date.now() - 14 * 24 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Maintenance window has begun. No customer impact expected.',
author: 'Storage Team'
},
{
id: 'update-11',
timestamp: Date.now() - 14 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
status: 'resolved',
message: 'Maintenance completed successfully.',
author: 'Storage Team'
}
]
},
{
id: 'inc-2023-089',
title: 'API Gateway Rate Limiting Issues',
impact: 'Some API requests were incorrectly rate limited',
severity: 'major',
status: 'resolved',
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
affectedServices: ['API Gateway'],
rootCause: 'A bug in the rate limiting algorithm caused legitimate requests to be blocked.',
resolution: 'Hotfix deployed to correct the rate limiting logic.',
updates: [
{
id: 'update-12',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Reports of API requests being blocked despite being within rate limits.',
author: 'API Team'
},
{
id: 'update-13',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000,
status: 'identified',
message: 'Bug identified in rate limiting code. Preparing hotfix.',
author: 'API Team'
},
{
id: 'update-14',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
status: 'resolved',
message: 'Hotfix deployed. Rate limiting now functioning correctly.',
author: 'API Team'
}
]
}
];
incidents.currentIncidents = currentIncidents;
incidents.pastIncidents = pastIncidents;
// Configure Stats Grid
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
const totalIncidents = currentIncidents.length + 3; // Current + recent past
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
statsGrid.currentStatus = 'degraded';
statsGrid.uptime = avgUptime;
statsGrid.avgResponseTime = Math.round(avgResponseTime);
statsGrid.totalIncidents = totalIncidents;
statsGrid.affectedServices = services.filter(s => s.currentStatus !== 'operational').length;
statsGrid.totalServices = services.length;
statsGrid.timePeriod = '30 days';
// Configure Footer
footer.companyName = 'CloudFlow Infrastructure';
footer.legalUrl = 'https://cloudflow.example.com/legal';
footer.supportEmail = 'support@cloudflow.example.com';
footer.statusPageUrl = 'https://status.cloudflow.example.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/cloudflow' },
{ platform: 'github', url: 'https://github.com/cloudflow' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/cloudflow' }
];
footer.rssFeedUrl = 'https://status.cloudflow.example.com/rss';
footer.apiStatusUrl = 'https://api.cloudflow.example.com/v1/status';
footer.enableSubscribe = true;
footer.subscriberCount = 2847;
footer.additionalLinks = [
{ label: 'API Documentation', url: 'https://docs.cloudflow.example.com' },
{ label: 'Service SLA', url: 'https://cloudflow.example.com/sla' },
{ label: 'Privacy Policy', url: 'https://cloudflow.example.com/privacy' }
];
// Add event listeners for interactivity
header.addEventListener('reportNewIncident', () => {
alert('Report Incident: This would open a form to report a new incident.');
});
header.addEventListener('statusSubscribe', () => {
alert('Subscribe: This would open a subscription form for status updates.');
});
footer.addEventListener('subscribeClick', () => {
alert('Subscribe to updates: Enter your email for status notifications.');
});
assetsSelector.addEventListener('selectionChanged', (event: CustomEvent) => {
console.log('Selected services changed:', event.detail.selectedServices);
// In a real app, this would update the other components to show only selected services
});
statusMonth.addEventListener('dayClick', (event: CustomEvent) => {
console.log('Day clicked:', event.detail);
alert(`Day details: ${event.detail.date}\nUptime: ${event.detail.uptime}%\nIncidents: ${event.detail.incidents}`);
});
// Simulate real-time updates
setInterval(() => {
// Update last checked times
services.forEach(service => {
service.lastChecked = Date.now();
// Randomly change response times
service.responseTime = service.responseTime * (0.9 + Math.random() * 0.2);
});
assetsSelector.requestUpdate();
// Update overall status last updated
statusBar.overallStatus = { ...statusBar.overallStatus, lastUpdated: Date.now() };
// Update footer last updated
footer.lastUpdated = Date.now();
}, 30000); // Every 30 seconds
}}
>
<upl-statuspage-header></upl-statuspage-header>
<upl-statuspage-pagetitle></upl-statuspage-pagetitle>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
<upl-statuspage-incidents></upl-statuspage-incidents>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
`;

View File

@@ -0,0 +1,570 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
export const statuspageMaintenance = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.demo-page-wrapper > dees-demowrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configure Header
header.pageTitle = 'SecureVault';
header.showReportButton = true;
header.showSubscribeButton = true;
header.logoUrl = 'https://via.placeholder.com/150x50/2196F3/ffffff?text=SV';
// Configure Overall Status - Maintenance
statusBar.overallStatus = {
status: 'maintenance',
message: 'Scheduled maintenance in progress - Expected completion: 2:00 AM UTC',
lastUpdated: Date.now(),
affectedServices: 5,
totalServices: 14
};
// Configure Services - Mix of maintenance and operational
const services: IServiceStatus[] = [
// Core Services - Some under maintenance
{
id: 'web-app',
name: 'web-app',
displayName: 'Web Application',
description: 'Customer portal',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.92,
uptime90d: 99.89,
responseTime: 45,
category: 'Frontend',
selected: true
},
{
id: 'mobile-api',
name: 'mobile-api',
displayName: 'Mobile API',
description: 'Mobile app backend',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.88,
uptime90d: 99.85,
responseTime: 62,
category: 'Frontend',
selected: true
},
{
id: 'auth-service',
name: 'auth-service',
displayName: 'Authentication Service',
description: 'User authentication',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.5,
uptime90d: 99.7,
responseTime: 0,
category: 'Core Services',
selected: true
},
{
id: 'api-gateway',
name: 'api-gateway',
displayName: 'API Gateway',
description: 'Main API endpoint',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 99.2,
uptime90d: 99.4,
responseTime: 125,
category: 'Core Services',
selected: true
},
// Database Services
{
id: 'primary-db',
name: 'primary-db',
displayName: 'Primary Database',
description: 'Main database cluster',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.6,
uptime90d: 99.7,
responseTime: 0,
category: 'Data Storage',
selected: true
},
{
id: 'replica-db',
name: 'replica-db',
displayName: 'Database Replicas',
description: 'Read replicas (read-only mode)',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 99.7,
uptime90d: 99.8,
responseTime: 85,
category: 'Data Storage',
selected: true
},
{
id: 'cache',
name: 'cache',
displayName: 'Cache Service',
description: 'Redis cache layer',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.93,
responseTime: 8,
category: 'Data Storage',
selected: false
},
// Backup and Storage
{
id: 'backup-service',
name: 'backup-service',
displayName: 'Backup Service',
description: 'Automated backups',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.8,
uptime90d: 99.85,
responseTime: 0,
category: 'Operations',
selected: true
},
{
id: 'file-storage',
name: 'file-storage',
displayName: 'File Storage',
description: 'Object storage service',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.98,
responseTime: 42,
category: 'Operations',
selected: false
},
// Monitoring and Support
{
id: 'monitoring',
name: 'monitoring',
displayName: 'Monitoring System',
description: 'System monitoring',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.88,
responseTime: 32,
category: 'Support',
selected: true
},
{
id: 'logging',
name: 'logging',
displayName: 'Logging Service',
description: 'Centralized logs',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.7,
uptime90d: 99.75,
responseTime: 0,
category: 'Support',
selected: false
},
{
id: 'alerting',
name: 'alerting',
displayName: 'Alerting System',
description: 'Alert notifications',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.85,
uptime90d: 99.82,
responseTime: 28,
category: 'Support',
selected: false
},
// Communication Services
{
id: 'email',
name: 'email',
displayName: 'Email Service',
description: 'Transactional emails',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.6,
uptime90d: 99.55,
responseTime: 142,
category: 'Communication',
selected: true
},
{
id: 'webhooks',
name: 'webhooks',
displayName: 'Webhook Delivery',
description: 'Webhook notifications',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.4,
uptime90d: 99.45,
responseTime: 0,
category: 'Communication',
selected: false
}
];
assetsSelector.services = services;
// Configure Stats Grid - Maintenance mode metrics
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
statsGrid.currentStatus = 'maintenance';
statsGrid.uptime = avgUptime;
statsGrid.avgResponseTime = Math.round(avgResponseTime);
statsGrid.totalIncidents = 1; // Just the maintenance
statsGrid.affectedServices = services.filter(s => s.currentStatus === 'maintenance').length;
statsGrid.totalServices = services.length;
statsGrid.timePeriod = '30 days';
// Configure Status Details - Showing maintenance period
const generateStatusDetails = (): IStatusHistoryPoint[] => {
const details: IStatusHistoryPoint[] = [];
const now = new Date();
const maintenanceStarted = 2; // 2 hours ago
for (let i = 47; i >= 0; i--) {
const date = new Date(now);
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
let status: 'operational' | 'degraded' | 'outage' = 'operational';
let value = 100;
let responseTime = 25;
// Maintenance window
if (i <= maintenanceStarted) {
status = 'degraded';
value = 60;
responseTime = 0;
} else if (i <= maintenanceStarted + 2) {
// Pre-maintenance prep
status = 'degraded';
value = 85;
responseTime = 80;
}
details.push({
timestamp: date.getTime(),
status,
responseTime
});
}
return details;
};
statusDetails.dataPoints = generateStatusDetails();
statusDetails.serviceId = 'primary-db';
statusDetails.serviceName = 'Primary Database';
// Configure Monthly Status - Good uptime with scheduled maintenance windows
const generateMonthlyData = (): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let m = 4; m >= 0; m--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
const monthKey = monthDate.toISOString().slice(0, 7);
const days = [];
let totalUptime = 0;
let incidents = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
const isToday = dayDate.toDateString() === now.toDateString();
const isFuture = dayDate > now;
const isFirstSunday = dayDate.getDay() === 0 && d <= 7;
if (isFuture) continue;
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
let uptime = 100;
let dayIncidents = 0;
// Today - maintenance
if (isToday) {
status = 'degraded';
uptime = 92; // 2 hours of maintenance = 8% downtime
dayIncidents = 0; // Maintenance is not an incident
} else if (isFirstSunday) {
// Monthly maintenance on first Sunday
status = 'degraded';
uptime = 96; // 1 hour maintenance
dayIncidents = 0;
} else if (Math.random() > 0.95) {
// Occasional issues
status = 'degraded';
uptime = 98 + Math.random() * 2;
dayIncidents = 1;
}
days.push({
date: dayDate.toISOString().slice(0, 10),
status,
uptime,
incidents: dayIncidents,
totalDowntime: Math.round((100 - uptime) * 14.4)
});
totalUptime += uptime;
incidents += dayIncidents;
}
months.push({
month: monthKey,
days,
overallUptime: totalUptime / days.length,
totalIncidents: incidents
});
}
return months;
};
statusMonth.monthlyData = generateMonthlyData();
statusMonth.serviceId = 'all-services';
statusMonth.serviceName = 'All Services';
// Configure Incidents - Current maintenance
const currentIncidents: IIncidentDetails[] = [
{
id: 'maint-2024-001',
title: 'Scheduled Database Maintenance',
impact: 'Database will be in read-only mode. Some features may be temporarily unavailable.',
severity: 'minor',
status: 'monitoring',
startTime: Date.now() - 2 * 60 * 60 * 1000,
affectedServices: ['Primary Database', 'Authentication Service', 'Backup Service', 'Logging Service', 'Webhook Delivery'],
updates: [
{
id: 'update-1',
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Scheduled maintenance has been announced for database upgrades and security patches.',
author: 'Operations Team'
},
{
id: 'update-2',
timestamp: Date.now() - 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Reminder: Database maintenance scheduled for tomorrow at 12:00 AM UTC.',
author: 'Operations Team'
},
{
id: 'update-3',
timestamp: Date.now() - 2 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Maintenance window has begun. Database is now in read-only mode.',
author: 'Database Team'
},
{
id: 'update-4',
timestamp: Date.now() - 1.5 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Security patches applied successfully. Beginning database schema updates.',
author: 'Database Team'
},
{
id: 'update-5',
timestamp: Date.now() - 1 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Schema updates 50% complete. Everything proceeding as planned.',
author: 'Database Team'
},
{
id: 'update-6',
timestamp: Date.now() - 30 * 60 * 1000,
status: 'monitoring',
message: 'Final testing phase. Services will begin coming back online shortly.',
author: 'Operations Team'
}
]
}
];
const pastIncidents: IIncidentDetails[] = [
{
id: 'maint-2024-prev-001',
title: 'Previous Monthly Maintenance',
impact: 'Services were in maintenance mode for 1 hour',
severity: 'minor',
status: 'resolved',
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000,
affectedServices: ['Primary Database', 'Backup Service'],
rootCause: 'Scheduled monthly maintenance for security updates.',
resolution: 'Maintenance completed successfully with all updates applied.',
updates: [
{
id: 'update-7',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Monthly maintenance started.',
author: 'Operations Team'
},
{
id: 'update-8',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000,
status: 'resolved',
message: 'Maintenance completed successfully.',
author: 'Operations Team'
}
]
},
{
id: 'inc-2024-001',
title: 'API Gateway Performance Issues',
impact: 'Slow API responses for some users',
severity: 'minor',
status: 'resolved',
startTime: Date.now() - 15 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 15 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
affectedServices: ['API Gateway'],
rootCause: 'Memory leak in API Gateway service.',
resolution: 'Service restarted and patch applied.',
updates: [
{
id: 'update-9',
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Investigating reports of slow API responses.',
author: 'API Team'
},
{
id: 'update-10',
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
status: 'identified',
message: 'Memory leak identified. Preparing fix.',
author: 'API Team'
},
{
id: 'update-11',
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
status: 'resolved',
message: 'Fix applied. Performance back to normal.',
author: 'API Team'
}
]
},
{
id: 'maint-2023-012',
title: 'Year-End Infrastructure Upgrade',
impact: 'Planned downtime for major infrastructure improvements',
severity: 'major',
status: 'resolved',
startTime: Date.now() - 60 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 60 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
affectedServices: ['All Services'],
rootCause: 'Annual infrastructure upgrade and capacity expansion.',
resolution: 'All upgrades completed successfully. System capacity increased by 50%.',
updates: [
{
id: 'update-12',
timestamp: Date.now() - 67 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Year-end maintenance scheduled for next week.',
author: 'CTO'
},
{
id: 'update-13',
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Maintenance window started. All services going offline.',
author: 'Operations Team'
},
{
id: 'update-14',
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Hardware upgrades complete. Software updates in progress.',
author: 'Infrastructure Team'
},
{
id: 'update-15',
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
status: 'resolved',
message: 'All systems back online. Performance improvements confirmed.',
author: 'Operations Team'
},
{
id: 'update-16',
timestamp: Date.now() - 59 * 24 * 60 * 60 * 1000,
status: 'postmortem',
message: 'Maintenance report published. 50% capacity increase achieved.',
author: 'Engineering Team'
}
]
}
];
incidents.currentIncidents = currentIncidents;
incidents.pastIncidents = pastIncidents;
// Configure Footer
footer.companyName = 'SecureVault Infrastructure';
footer.legalUrl = 'https://securevault.example.com/legal';
footer.supportEmail = 'support@securevault.example.com';
footer.statusPageUrl = 'https://status.securevault.example.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/securevault' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/securevault' }
];
footer.rssFeedUrl = 'https://status.securevault.example.com/rss';
footer.apiStatusUrl = 'https://api.securevault.example.com/v1/status';
footer.enableSubscribe = true;
footer.subscriberCount = 3892;
footer.additionalLinks = [
{ label: 'Maintenance Schedule', url: 'https://securevault.example.com/maintenance' },
{ label: 'API Documentation', url: 'https://docs.securevault.example.com' },
{ label: 'Support Portal', url: 'https://support.securevault.example.com' }
];
footer.latestStatusUpdate = 'Database maintenance 50% complete - On track for 2:00 AM UTC completion';
}}
>
<upl-statuspage-header></upl-statuspage-header>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
<upl-statuspage-incidents></upl-statuspage-incidents>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
`;

View File

@@ -0,0 +1,568 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
export const statuspageOutage = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.demo-page-wrapper > dees-demowrapper {
display: flex;
flex-direction: column;
gap: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configure Header
header.pageTitle = 'DataStream';
header.showReportButton = true;
header.showSubscribeButton = true;
header.logoUrl = 'https://via.placeholder.com/150x50/F44336/ffffff?text=DS';
// Configure Overall Status - Major Outage
statusBar.overallStatus = {
status: 'major_outage',
message: 'Major service disruption affecting multiple regions',
lastUpdated: Date.now(),
affectedServices: 8,
totalServices: 15
};
// Configure Services - Multiple Outages
const services: IServiceStatus[] = [
// Core Services - Major Outage
{
id: 'api-gateway',
name: 'api-gateway',
displayName: 'API Gateway',
description: 'Main API endpoint',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 85.2,
uptime90d: 92.5,
responseTime: 2500,
category: 'Core Infrastructure',
selected: true
},
{
id: 'auth-service',
name: 'auth-service',
displayName: 'Authentication',
description: 'Login and authorization',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 84.8,
uptime90d: 91.2,
responseTime: 3200,
category: 'Core Infrastructure',
selected: true
},
{
id: 'web-portal',
name: 'web-portal',
displayName: 'Web Portal',
description: 'Customer web interface',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 88.5,
uptime90d: 94.2,
responseTime: 1800,
category: 'Frontend',
selected: true
},
// Database Services - Critical
{
id: 'primary-db',
name: 'primary-db',
displayName: 'Primary Database',
description: 'Main data storage',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 82.1,
uptime90d: 90.5,
responseTime: 5000,
category: 'Data Layer',
selected: true
},
{
id: 'replica-db',
name: 'replica-db',
displayName: 'Database Replicas',
description: 'Read replicas',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 86.7,
uptime90d: 93.1,
responseTime: 2200,
category: 'Data Layer',
selected: true
},
{
id: 'cache-layer',
name: 'cache-layer',
displayName: 'Cache Layer',
description: 'Redis cache',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 92.3,
uptime90d: 95.8,
responseTime: 450,
category: 'Data Layer',
selected: true
},
// Regional Services
{
id: 'us-east',
name: 'us-east',
displayName: 'US East Region',
description: 'Primary US datacenter',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 81.5,
uptime90d: 89.2,
responseTime: 4500,
category: 'Regional',
selected: true
},
{
id: 'us-west',
name: 'us-west',
displayName: 'US West Region',
description: 'Secondary US datacenter',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 94.2,
uptime90d: 96.8,
responseTime: 180,
category: 'Regional',
selected: true
},
{
id: 'eu-central',
name: 'eu-central',
displayName: 'EU Central Region',
description: 'European datacenter',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 83.7,
uptime90d: 91.2,
responseTime: 3800,
category: 'Regional',
selected: true
},
// Some operational services
{
id: 'status-page',
name: 'status-page',
displayName: 'Status Page',
description: 'This status page',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.8,
responseTime: 45,
category: 'Support',
selected: false
},
{
id: 'support-portal',
name: 'support-portal',
displayName: 'Support Portal',
description: 'Customer support system',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 98.5,
uptime90d: 99.1,
responseTime: 120,
category: 'Support',
selected: false
},
// Degraded services
{
id: 'email-service',
name: 'email-service',
displayName: 'Email Notifications',
description: 'Automated emails',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 91.2,
uptime90d: 94.5,
responseTime: 850,
category: 'Communication',
selected: true
},
{
id: 'analytics',
name: 'analytics',
displayName: 'Analytics Platform',
description: 'Usage analytics',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 87.3,
uptime90d: 92.8,
responseTime: 1200,
category: 'Services',
selected: false
},
{
id: 'backup-system',
name: 'backup-system',
displayName: 'Backup System',
description: 'Data backups',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 95.2,
uptime90d: 97.1,
responseTime: 320,
category: 'Services',
selected: false
},
{
id: 'monitoring',
name: 'monitoring',
displayName: 'Monitoring System',
description: 'Infrastructure monitoring',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 98.7,
uptime90d: 99.2,
responseTime: 65,
category: 'Services',
selected: true
}
];
assetsSelector.services = services;
// Configure Stats Grid - Major outage metrics
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
statsGrid.currentStatus = 'major_outage';
statsGrid.uptime = avgUptime;
statsGrid.avgResponseTime = Math.round(avgResponseTime);
statsGrid.totalIncidents = 15; // High number of incidents
statsGrid.affectedServices = services.filter(s => s.currentStatus !== 'operational').length;
statsGrid.totalServices = services.length;
statsGrid.timePeriod = '30 days';
// Configure Status Details - Showing the outage timeline
const generateStatusDetails = (): IStatusHistoryPoint[] => {
const details: IStatusHistoryPoint[] = [];
const now = new Date();
for (let i = 47; i >= 0; i--) {
const date = new Date(now);
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
let status: 'operational' | 'degraded' | 'major_outage' = 'operational';
let value = 100;
let responseTime = 30;
// Outage started 8 hours ago
if (i <= 8) {
status = 'major_outage';
value = 15 + Math.random() * 20;
responseTime = 2000 + Math.random() * 3000;
} else if (i <= 12) {
// Degradation before the outage
status = 'degraded';
value = 70 + Math.random() * 20;
responseTime = 200 + Math.random() * 800;
} else if (i <= 24) {
// Some issues yesterday
if (Math.random() > 0.7) {
status = 'degraded';
value = 85 + Math.random() * 10;
responseTime = 100 + Math.random() * 200;
}
}
details.push({
timestamp: date.getTime(),
status,
responseTime
});
}
return details;
};
statusDetails.dataPoints = generateStatusDetails();
statusDetails.serviceId = 'primary-db';
statusDetails.serviceName = 'Primary Database';
// Configure Monthly Status - Poor performance
const generateMonthlyData = (): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let m = 4; m >= 0; m--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
const monthKey = monthDate.toISOString().slice(0, 7);
const days = [];
let totalUptime = 0;
let incidents = 0;
for (let d = 1; d <= daysInMonth; d++) {
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
const isToday = dayDate.toDateString() === now.toDateString();
const isFuture = dayDate > now;
if (isFuture) continue;
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
let uptime = 100;
let dayIncidents = 0;
// Today - major outage
if (isToday) {
status = 'major_outage';
uptime = 45;
dayIncidents = 3;
} else if (Math.random() > 0.7) {
// Frequent issues
if (Math.random() > 0.5) {
status = 'partial_outage';
uptime = 75 + Math.random() * 15;
dayIncidents = 2;
} else {
status = 'degraded';
uptime = 85 + Math.random() * 10;
dayIncidents = 1;
}
}
days.push({
date: dayDate.toISOString().slice(0, 10),
status,
uptime,
incidents: dayIncidents,
totalDowntime: Math.round((100 - uptime) * 14.4)
});
totalUptime += uptime;
incidents += dayIncidents;
}
months.push({
month: monthKey,
days,
overallUptime: totalUptime / days.length,
totalIncidents: incidents
});
}
return months;
};
statusMonth.monthlyData = generateMonthlyData();
statusMonth.serviceId = 'all-services';
statusMonth.serviceName = 'All Services';
// Configure Incidents - Multiple current incidents
const currentIncidents: IIncidentDetails[] = [
{
id: 'inc-2024-crit-001',
title: 'Complete Database Failure in Multiple Regions',
impact: 'Users unable to access any services. Complete system outage.',
severity: 'critical',
status: 'investigating',
startTime: Date.now() - 8 * 60 * 60 * 1000,
affectedServices: ['Primary Database', 'Database Replicas', 'API Gateway', 'Authentication', 'US East Region', 'EU Central Region'],
updates: [
{
id: 'update-1',
timestamp: Date.now() - 8 * 60 * 60 * 1000,
status: 'investigating',
message: 'Multiple alerts triggered. Complete service failure detected across regions.',
author: 'Automated Monitoring'
},
{
id: 'update-2',
timestamp: Date.now() - 7.5 * 60 * 60 * 1000,
status: 'investigating',
message: 'All hands on deck. Engineering teams mobilized. Initial investigation points to cascading database failure.',
author: 'Incident Commander'
},
{
id: 'update-3',
timestamp: Date.now() - 6 * 60 * 60 * 1000,
status: 'identified',
message: 'Root cause identified: Corrupted replication logs causing database cluster failures. Working on recovery.',
author: 'Database Team'
},
{
id: 'update-4',
timestamp: Date.now() - 4 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Attempting to restore from backups. This is a complex operation and will take time.',
author: 'Infrastructure Team'
},
{
id: 'update-5',
timestamp: Date.now() - 2 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Partial recovery in progress. Some read-only operations may become available soon.',
author: 'Database Team'
},
{
id: 'update-6',
timestamp: Date.now() - 30 * 60 * 1000,
status: 'monitoring',
message: 'Critical update: Recovery is taking longer than expected. We are exploring all options including failover to disaster recovery site.',
author: 'CTO'
}
]
},
{
id: 'inc-2024-maj-002',
title: 'API Gateway Overload',
impact: 'API requests failing or timing out',
severity: 'major',
status: 'monitoring',
startTime: Date.now() - 6 * 60 * 60 * 1000,
affectedServices: ['API Gateway', 'Web Portal'],
updates: [
{
id: 'update-7',
timestamp: Date.now() - 6 * 60 * 60 * 1000,
status: 'investigating',
message: 'API Gateway experiencing extreme load due to retry storms from database failures.',
author: 'API Team'
},
{
id: 'update-8',
timestamp: Date.now() - 5 * 60 * 60 * 1000,
status: 'identified',
message: 'Implementing rate limiting and circuit breakers to prevent cascade failures.',
author: 'API Team'
},
{
id: 'update-9',
timestamp: Date.now() - 3 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Rate limiting in place. Load is stabilizing but service remains degraded.',
author: 'API Team'
}
]
},
{
id: 'inc-2024-maj-003',
title: 'Email Service Delays',
impact: 'Notification emails delayed by up to 2 hours',
severity: 'major',
status: 'monitoring',
startTime: Date.now() - 5 * 60 * 60 * 1000,
affectedServices: ['Email Notifications'],
updates: [
{
id: 'update-10',
timestamp: Date.now() - 5 * 60 * 60 * 1000,
status: 'investigating',
message: 'Email queue backing up due to system outages.',
author: 'Communications Team'
},
{
id: 'update-11',
timestamp: Date.now() - 3 * 60 * 60 * 1000,
status: 'monitoring',
message: 'Processing backlog slowly. Prioritizing critical notifications.',
author: 'Communications Team'
}
]
}
];
const pastIncidents: IIncidentDetails[] = [
{
id: 'inc-2024-prev-001',
title: 'Previous Major Outage',
impact: 'Services unavailable for 4 hours',
severity: 'critical',
status: 'resolved',
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
affectedServices: ['All Services'],
rootCause: 'Power failure at primary datacenter with UPS failure.',
resolution: 'Power restored. UPS systems replaced and tested.',
updates: [
{
id: 'update-12',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
status: 'investigating',
message: 'Complete datacenter power loss.',
author: 'Operations'
},
{
id: 'update-13',
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
status: 'resolved',
message: 'Power restored. Services coming back online.',
author: 'Operations'
},
{
id: 'update-14',
timestamp: Date.now() - 29 * 24 * 60 * 60 * 1000,
status: 'postmortem',
message: 'Postmortem published. Multiple redundancy improvements implemented.',
author: 'Engineering'
}
]
}
];
incidents.currentIncidents = currentIncidents;
incidents.pastIncidents = pastIncidents;
// Configure Footer
footer.companyName = 'DataStream Platform';
footer.legalUrl = 'https://datastream.example.com/legal';
footer.supportEmail = 'emergency@datastream.example.com';
footer.statusPageUrl = 'https://status.datastream.example.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/datastream' }
];
footer.rssFeedUrl = 'https://status.datastream.example.com/rss';
footer.apiStatusUrl = 'https://api.datastream.example.com/v1/status';
footer.enableSubscribe = true;
footer.enableReportIssue = true;
footer.subscriberCount = 15234;
footer.errorMessage = 'Critical: Multiple services experiencing major outages';
footer.additionalLinks = [
{ label: 'Emergency Support', url: 'tel:+1-800-HELP-NOW' },
{ label: 'Incident Updates', url: 'https://datastream.example.com/incidents' }
];
}}
>
<upl-statuspage-header></upl-statuspage-header>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
<upl-statuspage-incidents></upl-statuspage-incidents>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
`;

View File

@@ -1,5 +1,5 @@
import * as domtools from '@designestate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import * as uplInterfaces from '@uptimelink/interfaces'; import * as uplInterfaces from '@uptime.link/interfaces';
export { export {
domtools, domtools,

View File

@@ -0,0 +1,531 @@
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', '#09090b'),
secondary: cssManager.bdTheme('#fafafa', '#18181b'),
muted: cssManager.bdTheme('#f4f4f5', '#27272a'),
card: cssManager.bdTheme('#ffffff', '#0f0f12'),
elevated: cssManager.bdTheme('#ffffff', '#1a1a1e')
},
// Border colors
border: {
default: cssManager.bdTheme('#e4e4e7', '#27272a'),
muted: cssManager.bdTheme('#f4f4f5', '#3f3f46'),
subtle: cssManager.bdTheme('#f0f0f2', '#1f1f23')
},
// Text colors
text: {
primary: cssManager.bdTheme('#09090b', '#fafafa'),
secondary: cssManager.bdTheme('#71717a', '#a1a1aa'),
muted: cssManager.bdTheme('#a1a1aa', '#71717a')
},
// Status colors - vibrant and accessible
status: {
operational: cssManager.bdTheme('#16a34a', '#22c55e'),
degraded: cssManager.bdTheme('#d97706', '#fbbf24'),
partial: cssManager.bdTheme('#dc2626', '#f87171'),
major: cssManager.bdTheme('#b91c1c', '#ef4444'),
maintenance: cssManager.bdTheme('#2563eb', '#60a5fa')
},
// Accent colors for interactive elements
accent: {
primary: cssManager.bdTheme('#09090b', '#fafafa'),
hover: cssManager.bdTheme('#18181b', '#e4e4e7'),
focus: cssManager.bdTheme('#3b82f6', '#60a5fa')
}
};
export const shadows = {
xs: '0 1px 2px 0 rgba(0, 0, 0, 0.03)',
sm: '0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.06)',
base: '0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05)',
md: '0 6px 12px -2px rgba(0, 0, 0, 0.08), 0 3px 7px -3px rgba(0, 0, 0, 0.05)',
lg: '0 12px 24px -4px rgba(0, 0, 0, 0.1), 0 6px 12px -6px rgba(0, 0, 0, 0.05)',
xl: '0 24px 48px -12px rgba(0, 0, 0, 0.12), 0 12px 24px -12px rgba(0, 0, 0, 0.05)',
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.04)',
glow: '0 0 20px -5px rgba(34, 197, 94, 0.3)'
};
export const borderRadius = {
xs: '3px',
sm: '4px',
base: '6px',
md: '8px',
lg: '12px',
xl: '16px',
'2xl': '24px',
full: '9999px'
};
export const spacing = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px',
'4xl': '96px'
};
// Animation easings
export const easings = {
default: 'cubic-bezier(0.4, 0, 0.2, 1)',
smooth: 'cubic-bezier(0.4, 0, 0.6, 1)',
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
snappy: 'cubic-bezier(0.2, 0, 0, 1)',
spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
};
// Durations
export const durations = {
instant: '50ms',
fast: '100ms',
normal: '200ms',
slow: '300ms',
slower: '500ms',
slowest: '800ms'
};
// Status gradients for backgrounds
export const statusGradients = {
operational: cssManager.bdTheme(
'linear-gradient(135deg, rgba(22, 163, 74, 0.08) 0%, rgba(22, 163, 74, 0.02) 100%)',
'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(34, 197, 94, 0.03) 100%)'
),
degraded: cssManager.bdTheme(
'linear-gradient(135deg, rgba(217, 119, 6, 0.08) 0%, rgba(217, 119, 6, 0.02) 100%)',
'linear-gradient(135deg, rgba(251, 191, 36, 0.12) 0%, rgba(251, 191, 36, 0.03) 100%)'
),
partial: cssManager.bdTheme(
'linear-gradient(135deg, rgba(220, 38, 38, 0.08) 0%, rgba(220, 38, 38, 0.02) 100%)',
'linear-gradient(135deg, rgba(248, 113, 113, 0.12) 0%, rgba(248, 113, 113, 0.03) 100%)'
),
major: cssManager.bdTheme(
'linear-gradient(135deg, rgba(185, 28, 28, 0.10) 0%, rgba(185, 28, 28, 0.03) 100%)',
'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(239, 68, 68, 0.04) 100%)'
),
maintenance: cssManager.bdTheme(
'linear-gradient(135deg, rgba(37, 99, 235, 0.08) 0%, rgba(37, 99, 235, 0.02) 100%)',
'linear-gradient(135deg, rgba(96, 165, 250, 0.12) 0%, rgba(96, 165, 250, 0.03) 100%)'
)
};
// Glassmorphism styles
export const glass = {
light: 'background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);',
dark: 'background: rgba(9, 9, 11, 0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);',
subtle: cssManager.bdTheme(
'background: rgba(255, 255, 255, 0.5); backdrop-filter: blur(8px);',
'background: rgba(9, 9, 11, 0.6); backdrop-filter: blur(8px);'
)
};
// Status glow shadows
export const statusGlows = {
operational: '0 0 20px -5px rgba(34, 197, 94, 0.4)',
degraded: '0 0 20px -5px rgba(251, 191, 36, 0.4)',
partial: '0 0 20px -5px rgba(248, 113, 113, 0.4)',
major: '0 0 20px -5px rgba(239, 68, 68, 0.5)',
maintenance: '0 0 20px -5px rgba(96, 165, 250, 0.4)'
};
export const commonStyles = css`
/* Button styles */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: ${unsafeCSS(fonts.base)};
font-size: 13px;
font-weight: 500;
height: 36px;
padding: 0 14px;
border-radius: ${unsafeCSS(borderRadius.base)};
border: 1px solid ${colors.border.default};
background: ${colors.background.primary};
color: ${colors.text.primary};
cursor: pointer;
user-select: none;
transition: all ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
gap: 6px;
white-space: nowrap;
letter-spacing: -0.01em;
}
.button:hover {
background: ${colors.background.secondary};
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
box-shadow: ${unsafeCSS(shadows.xs)};
}
.button:active {
transform: scale(0.98);
transition-duration: ${unsafeCSS(durations.fast)};
}
.button:focus-visible {
outline: 2px solid ${colors.accent.focus};
outline-offset: 2px;
}
.button.primary {
background: ${colors.accent.primary};
color: ${colors.background.primary};
border-color: transparent;
}
.button.primary:hover {
background: ${colors.accent.hover};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.button.ghost {
background: transparent;
border-color: transparent;
}
.button.ghost:hover {
background: ${colors.background.muted};
}
.button.sm {
height: 32px;
padding: 0 12px;
font-size: 12px;
}
.button.lg {
height: 44px;
padding: 0 20px;
font-size: 15px;
}
/* Card styles */
.card {
background: ${colors.background.card};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.lg)};
padding: ${unsafeCSS(spacing.lg)};
box-shadow: ${unsafeCSS(shadows.sm)};
transition: all ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
}
.card:hover {
border-color: ${colors.border.muted};
box-shadow: ${unsafeCSS(shadows.base)};
}
.card.interactive {
cursor: pointer;
}
.card.interactive:hover {
transform: translateY(-2px);
box-shadow: ${unsafeCSS(shadows.md)};
}
/* Loading skeleton */
.skeleton {
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f4f4f5 0%, #e4e4e7 50%, #f4f4f5 100%)',
'linear-gradient(90deg, #18181b 0%, #27272a 50%, #18181b 100%)'
)};
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: ${unsafeCSS(borderRadius.base)};
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Pulse animation for status indicators */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Pulse ring animation for active status */
@keyframes pulse-ring {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
/* Shimmer effect for loading states */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Fade in animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.default)} forwards;
}
/* Fade in up animation */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp ${unsafeCSS(durations.slower)} ${unsafeCSS(easings.default)} forwards;
}
/* Scale in animation */
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
.scale-in {
animation: scaleIn ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.bounce)} forwards;
}
/* Slide down animation for expanding content */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-down {
animation: slideDown ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.default)} forwards;
}
/* Stagger animation delay utilities */
.stagger-1 { animation-delay: 50ms; }
.stagger-2 { animation-delay: 100ms; }
.stagger-3 { animation-delay: 150ms; }
.stagger-4 { animation-delay: 200ms; }
.stagger-5 { animation-delay: 250ms; }
/* Status indicator with pulse ring */
.status-dot-animated {
position: relative;
width: 10px;
height: 10px;
border-radius: 50%;
}
.status-dot-animated::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: inherit;
animation: pulse-ring 2s ${unsafeCSS(easings.default)} infinite;
}
.status-dot-animated.operational::before {
background: ${colors.status.operational};
}
.status-dot-animated.degraded::before,
.status-dot-animated.partial_outage::before,
.status-dot-animated.major_outage::before {
animation: pulse-ring 1.5s ${unsafeCSS(easings.default)} infinite;
}
/* Hover lift effect */
.hover-lift {
transition: transform ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)},
box-shadow ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
}
.hover-lift:hover {
transform: translateY(-2px);
}
/* Icon animation */
.icon-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Bounce animation for attention */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.bounce {
animation: bounce 1s ${unsafeCSS(easings.bounce)} infinite;
}
/* Container styles */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)};
}
/* Status pill */
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: ${unsafeCSS(borderRadius.full)};
font-size: 12px;
font-weight: 500;
letter-spacing: 0.01em;
}
.status-pill .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.status-pill.operational {
background: ${cssManager.bdTheme('rgba(22, 163, 74, 0.1)', 'rgba(34, 197, 94, 0.15)')};
color: ${cssManager.bdTheme('#15803d', '#4ade80')};
}
.status-pill.operational .status-dot {
background: ${colors.status.operational};
}
.status-pill.degraded {
background: ${cssManager.bdTheme('rgba(217, 119, 6, 0.1)', 'rgba(251, 191, 36, 0.15)')};
color: ${cssManager.bdTheme('#b45309', '#fcd34d')};
}
.status-pill.degraded .status-dot {
background: ${colors.status.degraded};
}
.status-pill.partial_outage,
.status-pill.major_outage {
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(248, 113, 113, 0.15)')};
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
}
.status-pill.partial_outage .status-dot,
.status-pill.major_outage .status-dot {
background: ${colors.status.major};
}
.status-pill.maintenance {
background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.1)', 'rgba(96, 165, 250, 0.15)')};
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
}
.status-pill.maintenance .status-dot {
background: ${colors.status.maintenance};
}
/* Responsive utilities */
@media (max-width: 1024px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)};
}
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)};
}
.button {
height: 40px;
padding: 0 16px;
}
.button.sm {
height: 36px;
}
}
/* Visually hidden (for accessibility) */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;
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 '?';
}
};

View File

@@ -1,8 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "NodeNext",
"moduleResolution": "nodenext" "moduleResolution": "NodeNext",
} "esModuleInterop": true,
"verbatimModuleSyntax": true
},
"exclude": [
"dist_*/**/*.d.ts"
]
} }