feat(elements): add upladmin-option-card component and migrate option/status UIs to use it; refactor monitor form multitoggle subscriptions and event handling; improve theme color handling and dark-mode styles; add demos, Playwright snapshots, and migration plan

This commit is contained in:
2025-12-26 18:04:55 +00:00
parent e667370079
commit 6a7c69bad1
16 changed files with 869 additions and 152 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@uptime.link/statuspage-admin',
version: '1.1.0',
version: '1.2.0',
description: 'Admin components for managing UptimeLink status pages, monitors, and incidents.'
}

View File

@@ -1,3 +1,6 @@
// Shared components
export * from './upladmin-option-card/index.js';
// Monitor components
export * from './upladmin-monitor-form/index.js';
export * from './upladmin-monitor-list/index.js';

View File

@@ -168,51 +168,6 @@ export class UpladminIncidentForm extends DeesElement {
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.option-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${sharedStyles.colors.background.primary};
border: 2px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
text-align: center;
}
.option-card:hover {
border-color: ${sharedStyles.colors.border.strong};
background: ${sharedStyles.colors.background.muted};
}
.option-card.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.option-card input {
display: none;
}
.option-label {
font-size: 13px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
}
.option-desc {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
line-height: 1.3;
}
.severity-critical dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; }
.severity-major dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
.severity-minor dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
.severity-maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
.field-label {
display: block;
font-size: 13px;
@@ -363,20 +318,14 @@ export class UpladminIncidentForm extends DeesElement {
<label class="field-label required">Severity</label>
<div class="option-grid">
${severityOptions.map(opt => html`
<label
class="option-card severity-${opt.value} ${this.formData.severity === opt.value ? 'selected' : ''}"
@click="${() => this.handleSeverityChange(opt.value)}"
>
<input
type="radio"
name="severity"
value="${opt.value}"
?checked="${this.formData.severity === opt.value}"
/>
<dees-icon .icon=${this.severityIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="option-label">${opt.label}</span>
<span class="option-desc">${opt.desc}</span>
</label>
<upladmin-option-card
.variant=${opt.value}
.icon=${this.severityIcons[opt.value]}
.label=${opt.label}
.description=${opt.desc}
?selected=${this.formData.severity === opt.value}
@click=${() => this.handleSeverityChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>
@@ -385,19 +334,13 @@ export class UpladminIncidentForm extends DeesElement {
<label class="field-label required">Status</label>
<div class="option-grid">
${statusOptions.map(opt => html`
<label
class="option-card ${this.formData.status === opt.value ? 'selected' : ''}"
@click="${() => this.handleStatusChange(opt.value)}"
>
<input
type="radio"
name="status"
value="${opt.value}"
?checked="${this.formData.status === opt.value}"
/>
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="option-label">${opt.label}</span>
</label>
<upladmin-option-card
.variant=${opt.value}
.icon=${this.statusIcons[opt.value]}
.label=${opt.label}
?selected=${this.formData.status === opt.value}
@click=${() => this.handleStatusChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>

View File

@@ -260,8 +260,8 @@ export class UpladminIncidentList extends DeesElement {
.incident-status.postmortem {
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.2)')};
color: #a855f7;
--icon-color: #a855f7;
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
--icon-color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
.incident-meta {

View File

@@ -157,52 +157,6 @@ export class UpladminIncidentUpdate extends DeesElement {
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.status-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${sharedStyles.colors.background.primary};
border: 2px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
text-align: center;
}
.status-option:hover {
border-color: ${sharedStyles.colors.border.strong};
background: ${sharedStyles.colors.background.muted};
}
.status-option.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.status-option input {
display: none;
}
.status-option.investigating dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
.status-option.identified dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
.status-option.monitoring dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
.status-option.resolved dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; }
.status-option.postmortem dees-icon { --icon-color: #a855f7; }
.status-label {
font-size: 13px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
}
.status-desc {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
line-height: 1.3;
}
.field-label {
display: block;
font-size: 13px;
@@ -358,20 +312,14 @@ export class UpladminIncidentUpdate extends DeesElement {
<label class="field-label required">Status</label>
<div class="status-grid">
${statusOptions.map(opt => html`
<label
class="status-option ${opt.value} ${this.formData.status === opt.value ? 'selected' : ''}"
@click="${() => this.handleStatusChange(opt.value)}"
>
<input
type="radio"
name="status"
value="${opt.value}"
?checked="${this.formData.status === opt.value}"
/>
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="status-label">${opt.label}</span>
<span class="status-desc">${opt.desc}</span>
</label>
<upladmin-option-card
.variant=${opt.value}
.icon=${this.statusIcons[opt.value]}
.label=${opt.label}
.description=${opt.desc}
?selected=${this.formData.status === opt.value}
@click=${() => this.handleStatusChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>

View File

@@ -10,6 +10,7 @@ import {
unsafeCSS,
state,
} from '@design.estate/dees-element';
import type { DeesInputMultitoggle } from '@design.estate/dees-catalog';
import * as sharedStyles from '../../styles/shared.styles.js';
import type { IMonitorFormData, IServiceStatus, ICheckConfig, TStatusType, TCheckType, TStatusMode } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-monitor-form.demo.js';
@@ -60,6 +61,14 @@ export class UpladminMonitorForm extends DeesElement {
pagerank: 'PageRank',
};
private getCheckTypeLabel(): string {
return this.checkTypeLabels[this.formData.checkType] || 'Assumption';
}
private getStatusModeLabel(): string {
return this.formData.statusMode === 'auto' ? 'Auto' : 'Manual';
}
private intervalOptions = [
{ value: 60000, label: '1 min' },
{ value: 300000, label: '5 min' },
@@ -183,6 +192,8 @@ export class UpladminMonitorForm extends DeesElement {
`
];
private subscriptions: Array<{ unsubscribe: () => void }> = [];
async connectedCallback() {
await super.connectedCallback();
if (this.monitor) {
@@ -190,10 +201,47 @@ export class UpladminMonitorForm extends DeesElement {
}
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
async firstUpdated() {
await this.updateComplete;
this.setupMultitoggleSubscriptions();
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('monitor') && this.monitor) {
this.formData = { ...this.monitor };
}
// Re-setup subscriptions after each render in case elements changed
this.setupMultitoggleSubscriptions();
}
private subscribedElements = new WeakSet<Element>();
private setupMultitoggleSubscriptions() {
// Subscribe to check type toggle (only if not already subscribed)
const checkTypeToggle = this.shadowRoot?.querySelector('#checkTypeToggle') as DeesInputMultitoggle;
if (checkTypeToggle && !this.subscribedElements.has(checkTypeToggle)) {
this.subscribedElements.add(checkTypeToggle);
const sub = checkTypeToggle.changeSubject.subscribe(() => {
this.handleCheckTypeChange(checkTypeToggle.selectedOption);
});
this.subscriptions.push(sub);
}
// Subscribe to status mode toggle (only if not already subscribed)
const statusModeToggle = this.shadowRoot?.querySelector('#statusModeToggle') as DeesInputMultitoggle;
if (statusModeToggle && !this.subscribedElements.has(statusModeToggle)) {
this.subscribedElements.add(statusModeToggle);
const sub = statusModeToggle.changeSubject.subscribe(() => {
this.handleStatusModeChange(statusModeToggle.selectedOption);
});
this.subscriptions.push(sub);
}
}
public render(): TemplateResult {
@@ -266,10 +314,10 @@ export class UpladminMonitorForm extends DeesElement {
<!-- Check Type -->
<div class="form-section">
<dees-input-multitoggle
id="checkTypeToggle"
.label=${'Check Type'}
.options=${Object.values(this.checkTypeLabels)}
.selectedOption=${this.checkTypeLabels[this.formData.checkType]}
@changeSubject=${(e: CustomEvent) => this.handleCheckTypeChange(e.detail.selectedOption)}
.options=${['Assumption', 'Function', 'PWA', 'PageRank']}
.selectedOption=${this.getCheckTypeLabel()}
></dees-input-multitoggle>
</div>
@@ -293,18 +341,18 @@ export class UpladminMonitorForm extends DeesElement {
.label=${'Pause Monitor'}
.description=${'When paused, status will show as "paused" and checks won\'t run'}
.value=${this.formData.paused}
@changeSubject=${(e: CustomEvent) => this.updateField('paused', e.detail.value)}
@newValue=${(e: CustomEvent) => this.updateField('paused', e.detail)}
></dees-input-checkbox>
<!-- Status Override (Edit mode only) -->
${isEdit ? html`
<div class="form-section">
<dees-input-multitoggle
id="statusModeToggle"
.label=${'Status Mode'}
.description=${'Auto uses check results, Manual lets you override'}
.options=${['Auto', 'Manual']}
.selectedOption=${this.formData.statusMode === 'auto' ? 'Auto' : 'Manual'}
@changeSubject=${(e: CustomEvent) => this.handleStatusModeChange(e.detail.selectedOption)}
.selectedOption=${this.getStatusModeLabel()}
></dees-input-multitoggle>
${this.formData.statusMode === 'manual' ? html`
@@ -319,7 +367,7 @@ export class UpladminMonitorForm extends DeesElement {
]}
.selectedOption=${this.formData.manualStatus || 'operational'}
.direction=${'horizontal'}
@changeSubject=${(e: CustomEvent) => this.updateField('manualStatus', e.detail.selectedOption)}
@change=${(e: CustomEvent) => this.updateField('manualStatus', e.detail.value)}
></dees-input-radiogroup>
` : ''}
</div>
@@ -435,12 +483,12 @@ export class UpladminMonitorForm extends DeesElement {
<dees-input-checkbox
.label=${'Google'}
.value=${config.checkGoogle !== false}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('checkGoogle', e.detail.value)}
@newValue=${(e: CustomEvent) => this.updateCheckConfig('checkGoogle', e.detail)}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Bing'}
.value=${config.checkBing === true}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('checkBing', e.detail.value)}
@newValue=${(e: CustomEvent) => this.updateCheckConfig('checkBing', e.detail)}
></dees-input-checkbox>
</div>
<div class="form-row">

View File

@@ -128,6 +128,12 @@ export class UpladminMonitorList extends DeesElement {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.15)')};
}
@media (prefers-color-scheme: dark) {
.filter-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a1a1aa' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
}
}
.table-container {
padding: 0;
}
@@ -189,20 +195,20 @@ export class UpladminMonitorList extends DeesElement {
.status-badge.initializing {
background: ${cssManager.bdTheme('rgba(107, 114, 128, 0.1)', 'rgba(107, 114, 128, 0.15)')};
color: #6b7280;
--icon-color: #6b7280;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
--icon-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
.status-badge.error {
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(220, 38, 38, 0.15)')};
color: #dc2626;
--icon-color: #dc2626;
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.status-badge.paused {
background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.1)', 'rgba(139, 92, 246, 0.15)')};
color: #8b5cf6;
--icon-color: #8b5cf6;
color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
--icon-color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
}
/* Status indicators for override and pause */
@@ -224,12 +230,12 @@ export class UpladminMonitorList extends DeesElement {
.status-indicator.override {
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.15)', 'rgba(234, 179, 8, 0.2)')};
--icon-color: #eab308;
--icon-color: ${cssManager.bdTheme('#d97706', '#fbbf24')};
}
.status-indicator.paused {
background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.15)', 'rgba(139, 92, 246, 0.2)')};
--icon-color: #8b5cf6;
--icon-color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
}
.monitor-info {

View File

@@ -0,0 +1 @@
export * from './upladmin-option-card.js';

View File

@@ -0,0 +1,118 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { UpladminOptionCard } from './upladmin-option-card.js';
export const demoFunc = () => html`
<style>
.demo-container {
padding: 24px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
min-height: 100vh;
}
.demo-section {
margin-bottom: 32px;
}
.demo-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin-bottom: 16px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
}
</style>
<div class="demo-container">
<div class="demo-section">
<div class="demo-title">Severity Variants</div>
<div class="demo-grid">
<upladmin-option-card
variant="critical"
icon="lucide:AlertCircle"
label="Critical"
description="Severe impact"
></upladmin-option-card>
<upladmin-option-card
variant="major"
icon="lucide:AlertTriangle"
label="Major"
description="Significant impact"
></upladmin-option-card>
<upladmin-option-card
variant="minor"
icon="lucide:Info"
label="Minor"
description="Limited impact"
></upladmin-option-card>
<upladmin-option-card
variant="maintenance"
icon="lucide:Wrench"
label="Maintenance"
description="Scheduled work"
selected
></upladmin-option-card>
</div>
</div>
<div class="demo-section">
<div class="demo-title">Status Variants</div>
<div class="demo-grid">
<upladmin-option-card
variant="investigating"
icon="lucide:Search"
label="Investigating"
description="Looking into it"
></upladmin-option-card>
<upladmin-option-card
variant="identified"
icon="lucide:Target"
label="Identified"
description="Root cause found"
></upladmin-option-card>
<upladmin-option-card
variant="monitoring"
icon="lucide:Eye"
label="Monitoring"
description="Fix applied"
selected
></upladmin-option-card>
<upladmin-option-card
variant="resolved"
icon="lucide:CheckCircle"
label="Resolved"
description="Issue fixed"
></upladmin-option-card>
<upladmin-option-card
variant="postmortem"
icon="lucide:FileText"
label="Postmortem"
description="Analysis complete"
></upladmin-option-card>
</div>
</div>
<div class="demo-section">
<div class="demo-title">States</div>
<div class="demo-grid">
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Normal"
></upladmin-option-card>
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Selected"
selected
></upladmin-option-card>
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Disabled"
disabled
></upladmin-option-card>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,178 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import * as sharedStyles from '../../styles/shared.styles.js';
import { demoFunc } from './upladmin-option-card.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upladmin-option-card': UpladminOptionCard;
}
}
export type TOptionVariant =
// Severity variants
| 'critical' | 'major' | 'minor' | 'maintenance'
// Status variants
| 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem'
// Generic variants
| 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
@customElement('upladmin-option-card')
export class UpladminOptionCard extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor icon: string = '';
@property({ type: String })
accessor label: string = '';
@property({ type: String })
accessor description: string = '';
@property({ type: String, reflect: true })
accessor variant: TOptionVariant = 'default';
@property({ type: Boolean, reflect: true })
accessor selected: boolean = false;
@property({ type: Boolean, reflect: true })
accessor disabled: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.option-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 2px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
cursor: pointer;
transition: all 0.1s ease;
text-align: center;
user-select: none;
}
.option-card:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
:host([selected]) .option-card {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
:host([disabled]) .option-card {
opacity: 0.5;
cursor: not-allowed;
}
.option-label {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.option-desc {
font-size: 11px;
color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
line-height: 1.3;
}
/* Variant icon colors - all using bdTheme for proper light/dark support */
/* Severity variants */
:host([variant="critical"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
:host([variant="major"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
:host([variant="minor"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="maintenance"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
/* Status variants */
:host([variant="investigating"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
:host([variant="identified"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="monitoring"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
:host([variant="resolved"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
:host([variant="postmortem"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
/* Generic variants */
:host([variant="default"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
:host([variant="primary"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
:host([variant="success"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
:host([variant="warning"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="danger"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
:host([variant="info"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
dees-icon {
color: var(--icon-color);
}
`,
];
public render(): TemplateResult {
return html`
<div class="option-card ${this.disabled ? 'disabled' : ''}" @click="${this.handleClick}">
${this.icon ? html`<dees-icon .icon=${this.icon} .iconSize=${24}></dees-icon>` : ''}
${this.label ? html`<span class="option-label">${this.label}</span>` : ''}
${this.description ? html`<span class="option-desc">${this.description}</span>` : ''}
</div>
`;
}
private handleClick() {
if (this.disabled) return;
this.dispatchEvent(new CustomEvent('select', {
detail: { selected: !this.selected },
bubbles: true,
composed: true,
}));
}
}