This commit is contained in:
2025-12-24 10:57:43 +00:00
commit ba79b4bfb6
118 changed files with 292546 additions and 0 deletions

View File

@@ -0,0 +1 @@
export * from './upladmin-statuspage-config.js';

View File

@@ -0,0 +1,47 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { IStatusPageConfig } from '../../interfaces/index.js';
import './upladmin-statuspage-config.js';
export const demoFunc = () => html`
<style>
${css`
.demo-container {
padding: 24px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
min-height: 100vh;
}
.demo-title {
margin: 0 0 24px 0;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
text-transform: uppercase;
letter-spacing: 0.05em;
}
`}
</style>
<div class="demo-container">
<h3 class="demo-title">Status Page Configuration</h3>
<upladmin-statuspage-config
.config=${{
companyName: 'Acme Corporation',
companyLogo: 'https://via.placeholder.com/200x60?text=ACME',
supportEmail: 'support@acme.example.com',
statusPageUrl: 'https://status.acme.example.com',
legalUrl: 'https://acme.example.com/terms',
apiEndpoint: 'https://api.acme.example.com/status',
theme: 'auto',
whitelabel: false,
refreshInterval: 60,
showHistoricalDays: 90,
enableWebSocket: true,
enableNotifications: false,
timeZone: 'America/New_York',
language: 'en',
dateFormat: 'relative',
} as IStatusPageConfig}
></upladmin-statuspage-config>
</div>
`;

View File

@@ -0,0 +1,717 @@
import * as plugins from '../../plugins.js';
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
state,
} from '@design.estate/dees-element';
import * as sharedStyles from '../../styles/shared.styles.js';
import type { IStatusPageConfig } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-statuspage-config.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upladmin-statuspage-config': UpladminStatuspageConfig;
}
}
@customElement('upladmin-statuspage-config')
export class UpladminStatuspageConfig extends DeesElement {
public static demo = demoFunc;
@property({ type: Object })
accessor config: IStatusPageConfig = {};
@property({ type: Boolean })
accessor loading: boolean = false;
@state()
accessor formData: IStatusPageConfig = {};
@state()
accessor activeSection: string = 'branding';
@state()
accessor hasChanges: boolean = false;
public static styles = [
plugins.domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.config-container {
display: grid;
grid-template-columns: 220px 1fr;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
min-height: 500px;
}
@media (max-width: 768px) {
.config-container {
grid-template-columns: 1fr;
}
}
.config-nav {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
height: fit-content;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 16px;
font-size: 14px;
font-weight: 500;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.secondary};
background: transparent;
border: none;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
text-align: left;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.nav-item:hover {
background: ${sharedStyles.colors.background.muted};
color: ${sharedStyles.colors.text.primary};
}
.nav-item.active {
background: ${sharedStyles.colors.accent.primary};
color: white;
}
.nav-item.active dees-icon {
--icon-color: white;
}
.nav-item dees-icon {
--icon-color: ${sharedStyles.colors.text.muted};
transition: color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.nav-item:hover dees-icon {
--icon-color: ${sharedStyles.colors.text.primary};
}
.config-content {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
overflow: hidden;
}
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
border-bottom: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.muted};
}
.content-title {
font-size: 16px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
}
.content-subtitle {
font-size: 13px;
color: ${sharedStyles.colors.text.muted};
margin-top: 2px;
}
.save-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
color: ${sharedStyles.colors.accent.warning};
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.15)')};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
}
.save-indicator dees-icon {
--icon-color: ${sharedStyles.colors.accent.warning};
}
.content-body {
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
}
dees-form {
display: contents;
}
.form-section {
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xl)};
}
.form-section:last-child {
margin-bottom: 0;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
padding-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
border-bottom: 1px solid ${sharedStyles.colors.border.light};
}
.section-title dees-icon {
--icon-color: ${sharedStyles.colors.text.muted};
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
.content-actions {
display: flex;
justify-content: flex-end;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
border-top: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.muted};
}
.theme-options {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.theme-option {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px;
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)};
}
.theme-option:hover {
border-color: ${sharedStyles.colors.border.strong};
}
.theme-option.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.theme-option input {
display: none;
}
.theme-preview {
width: 56px;
height: 36px;
border-radius: 6px;
border: 1px solid ${sharedStyles.colors.border.default};
overflow: hidden;
}
.theme-preview.light {
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
}
.theme-preview.dark {
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
}
.theme-preview.auto {
background: linear-gradient(135deg, #f8fafc 0%, #f8fafc 50%, #1e293b 50%, #1e293b 100%);
}
.theme-label {
font-size: 13px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
}
.logo-preview {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
padding: ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.primary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.logo-preview img {
max-width: 140px;
max-height: 48px;
object-fit: contain;
}
.logo-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: ${sharedStyles.colors.text.muted};
font-size: 13px;
}
.logo-placeholder dees-icon {
--icon-color: ${sharedStyles.colors.text.muted};
opacity: 0.5;
}
.toggle-row {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
padding: ${unsafeCSS(sharedStyles.spacing.sm)} 0;
}
.toggle-label {
flex: 1;
}
.toggle-label-text {
font-size: 14px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
}
.toggle-label-hint {
font-size: 12px;
color: ${sharedStyles.colors.text.muted};
margin-top: 2px;
}
/* Style dees-input components */
dees-input-text,
dees-input-dropdown {
--dees-input-background: ${sharedStyles.colors.background.primary};
--dees-input-border-color: ${sharedStyles.colors.border.default};
}
`
];
async connectedCallback() {
await super.connectedCallback();
this.formData = { ...this.config };
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('config')) {
this.formData = { ...this.config };
this.hasChanges = false;
}
}
public render(): TemplateResult {
const sections = [
{ id: 'branding', icon: 'lucide:Palette', label: 'Branding', subtitle: 'Logo, company name, colors' },
{ id: 'urls', icon: 'lucide:Link', label: 'URLs', subtitle: 'Links and endpoints' },
{ id: 'behavior', icon: 'lucide:Settings', label: 'Behavior', subtitle: 'Refresh, notifications, history' },
{ id: 'advanced', icon: 'lucide:Wrench', label: 'Advanced', subtitle: 'API, timezone, language' },
];
return html`
<div class="config-container">
<nav class="config-nav">
${sections.map(section => html`
<button
class="nav-item ${this.activeSection === section.id ? 'active' : ''}"
@click="${() => this.activeSection = section.id}"
>
<dees-icon .icon=${section.icon} .iconSize=${18}></dees-icon>
<span>${section.label}</span>
</button>
`)}
</nav>
<div class="config-content">
<div class="content-header">
<div>
<div class="content-title">${sections.find(s => s.id === this.activeSection)?.label}</div>
<div class="content-subtitle">${sections.find(s => s.id === this.activeSection)?.subtitle}</div>
</div>
${this.hasChanges ? html`
<div class="save-indicator">
<dees-icon .icon=${'lucide:AlertCircle'} .iconSize=${14}></dees-icon>
<span>Unsaved changes</span>
</div>
` : ''}
</div>
<div class="content-body">
<dees-form>
${this.renderSection()}
</dees-form>
</div>
<div class="content-actions">
<dees-button type="discreet" @click="${this.handleReset}" ?disabled="${!this.hasChanges || this.loading}">
<dees-icon .icon=${'lucide:RotateCcw'} .iconSize=${14}></dees-icon>
Reset
</dees-button>
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${!this.hasChanges || this.loading}">
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:Save'} .iconSize=${16}></dees-icon>`}
Save Changes
</dees-button>
</div>
</div>
</div>
`;
}
private renderSection(): TemplateResult {
switch (this.activeSection) {
case 'branding':
return this.renderBrandingSection();
case 'urls':
return this.renderUrlsSection();
case 'behavior':
return this.renderBehaviorSection();
case 'advanced':
return this.renderAdvancedSection();
default:
return html``;
}
}
private renderBrandingSection(): TemplateResult {
const themeOptions: Array<{ value: 'light' | 'dark' | 'auto'; label: string; icon: string }> = [
{ value: 'light', label: 'Light', icon: 'lucide:Sun' },
{ value: 'dark', label: 'Dark', icon: 'lucide:Moon' },
{ value: 'auto', label: 'Auto', icon: 'lucide:Monitor' },
];
return html`
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Building'} .iconSize=${16}></dees-icon>
Company Information
</div>
<div class="form-grid">
<dees-input-text
key="companyName"
label="Company Name"
.value="${this.formData.companyName || ''}"
placeholder="Your Company"
description="Displayed in the header and footer"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('companyName', e.detail)}"
></dees-input-text>
<dees-input-text
key="supportEmail"
label="Support Email"
.value="${this.formData.supportEmail || ''}"
placeholder="support@example.com"
description="Contact email for users"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('supportEmail', e.detail)}"
></dees-input-text>
</div>
<dees-input-text
key="companyLogo"
label="Company Logo URL"
.value="${this.formData.companyLogo || ''}"
placeholder="https://example.com/logo.png"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('companyLogo', e.detail)}"
></dees-input-text>
${this.formData.companyLogo ? html`
<div class="logo-preview">
<img src="${this.formData.companyLogo}" alt="Company logo" @error="${this.handleLogoError}" />
</div>
` : html`
<div class="logo-preview">
<div class="logo-placeholder">
<dees-icon .icon=${'lucide:Image'} .iconSize=${20}></dees-icon>
No logo configured
</div>
</div>
`}
</div>
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Palette'} .iconSize=${16}></dees-icon>
Theme
</div>
<div class="theme-options">
${themeOptions.map(opt => html`
<label
class="theme-option ${this.formData.theme === opt.value ? 'selected' : ''}"
@click="${() => this.handleThemeChange(opt.value)}"
>
<input type="radio" name="theme" value="${opt.value}" ?checked="${this.formData.theme === opt.value}" />
<div class="theme-preview ${opt.value}"></div>
<span class="theme-label">${opt.label}</span>
</label>
`)}
</div>
</div>
<div class="form-section">
<div class="toggle-row">
<div class="toggle-label">
<div class="toggle-label-text">White Label Mode</div>
<div class="toggle-label-hint">Hide 'Powered by' branding</div>
</div>
<dees-input-checkbox
key="whitelabel"
.value="${this.formData.whitelabel || false}"
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('whitelabel', e.detail)}"
></dees-input-checkbox>
</div>
</div>
`;
}
private renderUrlsSection(): TemplateResult {
return html`
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Globe'} .iconSize=${16}></dees-icon>
Status Page URLs
</div>
<div class="form-grid">
<dees-input-text
key="statusPageUrl"
label="Status Page URL"
.value="${this.formData.statusPageUrl || ''}"
placeholder="https://status.example.com"
description="Public URL of your status page"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('statusPageUrl', e.detail)}"
></dees-input-text>
<dees-input-text
key="legalUrl"
label="Legal / Terms URL"
.value="${this.formData.legalUrl || ''}"
placeholder="https://example.com/terms"
description="Link to terms of service or legal info"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('legalUrl', e.detail)}"
></dees-input-text>
</div>
</div>
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Server'} .iconSize=${16}></dees-icon>
API Configuration
</div>
<dees-input-text
key="apiEndpoint"
label="API Endpoint"
.value="${this.formData.apiEndpoint || ''}"
placeholder="https://api.example.com/status"
description="Base URL for status API requests"
@changeSubject="${(e: CustomEvent) => this.handleTextChange('apiEndpoint', e.detail)}"
></dees-input-text>
</div>
`;
}
private renderBehaviorSection(): TemplateResult {
return html`
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:RefreshCw'} .iconSize=${16}></dees-icon>
Auto-refresh
</div>
<dees-input-text
key="refreshInterval"
label="Refresh Interval (seconds)"
inputType="number"
.value="${String(this.formData.refreshInterval || 60)}"
placeholder="60"
description="How often to refresh status data (minimum 30 seconds)"
@changeSubject="${(e: CustomEvent) => this.handleNumberChange('refreshInterval', e.detail)}"
></dees-input-text>
</div>
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:History'} .iconSize=${16}></dees-icon>
History
</div>
<dees-input-text
key="showHistoricalDays"
label="Historical Days to Show"
inputType="number"
.value="${String(this.formData.showHistoricalDays || 90)}"
placeholder="90"
description="Number of days of history to display"
@changeSubject="${(e: CustomEvent) => this.handleNumberChange('showHistoricalDays', e.detail)}"
></dees-input-text>
</div>
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Zap'} .iconSize=${16}></dees-icon>
Features
</div>
<div class="toggle-row">
<div class="toggle-label">
<div class="toggle-label-text">WebSocket Updates</div>
<div class="toggle-label-hint">Enable real-time updates</div>
</div>
<dees-input-checkbox
key="enableWebSocket"
.value="${this.formData.enableWebSocket || false}"
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('enableWebSocket', e.detail)}"
></dees-input-checkbox>
</div>
<div class="toggle-row">
<div class="toggle-label">
<div class="toggle-label-text">Browser Notifications</div>
<div class="toggle-label-hint">Allow push notifications</div>
</div>
<dees-input-checkbox
key="enableNotifications"
.value="${this.formData.enableNotifications || false}"
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('enableNotifications', e.detail)}"
></dees-input-checkbox>
</div>
</div>
`;
}
private renderAdvancedSection(): TemplateResult {
const timezoneOptions = [
{ key: 'UTC', option: 'UTC', payload: null },
{ key: 'America/New_York', option: 'Eastern Time (US)', payload: null },
{ key: 'America/Los_Angeles', option: 'Pacific Time (US)', payload: null },
{ key: 'Europe/London', option: 'London', payload: null },
{ key: 'Europe/Berlin', option: 'Berlin', payload: null },
{ key: 'Asia/Tokyo', option: 'Tokyo', payload: null },
{ key: 'Asia/Shanghai', option: 'Shanghai', payload: null },
];
const dateFormatOptions = [
{ key: 'relative', option: 'Relative (2 hours ago)', payload: null },
{ key: 'absolute', option: 'Absolute (Dec 23, 2024 14:30)', payload: null },
{ key: 'iso', option: 'ISO (2024-12-23T14:30:00)', payload: null },
];
const languageOptions = [
{ key: 'en', option: 'English', payload: null },
{ key: 'de', option: 'German', payload: null },
{ key: 'fr', option: 'French', payload: null },
{ key: 'es', option: 'Spanish', payload: null },
{ key: 'ja', option: 'Japanese', payload: null },
{ key: 'zh', option: 'Chinese', payload: null },
];
return html`
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:Globe2'} .iconSize=${16}></dees-icon>
Localization
</div>
<div class="form-grid">
<dees-input-dropdown
key="timeZone"
label="Timezone"
.options="${timezoneOptions}"
.selectedOption="${this.formData.timeZone || 'UTC'}"
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('timeZone', e.detail)}"
></dees-input-dropdown>
<dees-input-dropdown
key="language"
label="Language"
.options="${languageOptions}"
.selectedOption="${this.formData.language || 'en'}"
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('language', e.detail)}"
></dees-input-dropdown>
<dees-input-dropdown
key="dateFormat"
label="Date Format"
.options="${dateFormatOptions}"
.selectedOption="${this.formData.dateFormat || 'relative'}"
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('dateFormat', e.detail)}"
></dees-input-dropdown>
</div>
</div>
`;
}
private handleTextChange(name: string, value: string) {
this.formData = { ...this.formData, [name]: value };
this.hasChanges = true;
}
private handleNumberChange(name: string, value: string) {
this.formData = { ...this.formData, [name]: parseInt(value, 10) || 0 };
this.hasChanges = true;
}
private handleBooleanChange(name: string, value: boolean) {
this.formData = { ...this.formData, [name]: value };
this.hasChanges = true;
}
private handleDropdownChange(name: string, value: string) {
this.formData = { ...this.formData, [name]: value };
this.hasChanges = true;
}
private handleThemeChange(theme: 'light' | 'dark' | 'auto') {
this.formData = { ...this.formData, theme };
this.hasChanges = true;
}
private handleLogoError(e: Event) {
const img = e.target as HTMLImageElement;
img.style.display = 'none';
}
private handleSave() {
this.dispatchEvent(new CustomEvent('configSave', {
detail: { config: { ...this.formData } },
bubbles: true,
composed: true
}));
}
private handleReset() {
this.formData = { ...this.config };
this.hasChanges = false;
}
public setConfig(config: IStatusPageConfig) {
this.formData = { ...config };
this.hasChanges = false;
}
}