-
[ SPEEDTEST ]
-
-
- ${m.speedtest.isOnline ? 'Online' : 'Offline'}
-
- ${this.speedtestRunning ? html`
-
-
-
+
Speedtest
+
+
+
+ ${m.speedtest.isOnline ? 'Online' : 'Offline'}
+
+ ${this.speedtestRunning ? html`
+
+ ` : html`
+
+
+
${m.speedtest.lastDownloadSpeedMbps.toFixed(1)}
+
Mbps
+
Download
+
+
+
${m.speedtest.lastUploadSpeedMbps.toFixed(1)}
+
Mbps
+
Upload
+
+
+
${m.speedtest.lastLatencyMs.toFixed(0)}
+
ms
+
Latency
+
+
+ `}
+
+
- ` : html`
-
Download:${m.speedtest.lastDownloadSpeedMbps.toFixed(2)} Mbps
-
-
Upload:${m.speedtest.lastUploadSpeedMbps.toFixed(2)} Mbps
-
-
Latency:${m.speedtest.lastLatencyMs.toFixed(0)} ms
- `}
-
-
diff --git a/ts_swdash/sw-dash-styles.ts b/ts_swdash/sw-dash-styles.ts
index 0243c3d..7c80bf9 100644
--- a/ts_swdash/sw-dash-styles.ts
+++ b/ts_swdash/sw-dash-styles.ts
@@ -2,29 +2,58 @@ import { css } from './plugins.js';
import type { CSSResult } from './plugins.js';
/**
- * Shared terminal-style theme for sw-dash components
+ * Modern professional theme for sw-dash components
+ * Inspired by Bloomberg terminals, Vercel dashboards, and shadcn/ui
*/
export const sharedStyles: CSSResult = css`
:host {
- --sw-bg-dark: #0a0a0a;
- --sw-bg-panel: #0d0d0d;
- --sw-bg-header: #111;
- --sw-bg-input: #1a1a1a;
- --sw-border: #333;
- --sw-border-active: #00ff00;
- --sw-text-primary: #00ff00;
- --sw-text-secondary: #888;
- --sw-text-cyan: #00ffff;
- --sw-text-warning: #ffff00;
- --sw-text-error: #ff4444;
- --sw-gauge-good: #00aa00;
- --sw-gauge-warning: #aaaa00;
- --sw-gauge-bad: #aa0000;
+ /* Neutral backgrounds - zinc scale */
+ --bg-primary: #09090b;
+ --bg-secondary: #18181b;
+ --bg-tertiary: #27272a;
+ --bg-elevated: #3f3f46;
- font-family: 'Courier New', Courier, monospace;
- font-size: 14px;
- line-height: 1.4;
- color: var(--sw-text-primary);
+ /* Text colors */
+ --text-primary: #fafafa;
+ --text-secondary: #a1a1aa;
+ --text-tertiary: #71717a;
+
+ /* Borders */
+ --border-default: #27272a;
+ --border-muted: #3f3f46;
+
+ /* Accent colors */
+ --accent-primary: #3b82f6;
+ --accent-success: #22c55e;
+ --accent-warning: #eab308;
+ --accent-error: #ef4444;
+ --accent-info: #06b6d4;
+
+ /* Spacing scale */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-8: 32px;
+
+ /* Border radius */
+ --radius-sm: 4px;
+ --radius-md: 6px;
+ --radius-lg: 8px;
+ --radius-xl: 12px;
+
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+
+ * {
+ box-sizing: border-box;
}
`;
@@ -32,123 +61,129 @@ export const terminalStyles: CSSResult = css`
.terminal {
max-width: 1200px;
margin: 0 auto;
- border: 1px solid var(--sw-border-active);
- background: var(--sw-bg-panel);
- box-shadow: 0 0 20px rgba(0, 255, 0, 0.1);
+ border: 1px solid var(--border-default);
+ background: var(--bg-primary);
+ border-radius: var(--radius-lg);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ overflow: hidden;
}
.header {
- border-bottom: 1px solid var(--sw-border-active);
- padding: 10px 15px;
+ border-bottom: 1px solid var(--border-default);
+ padding: var(--space-4) var(--space-5);
display: flex;
justify-content: space-between;
align-items: center;
- background: var(--sw-bg-header);
+ background: var(--bg-secondary);
}
.title {
- color: var(--sw-text-primary);
- font-weight: bold;
- font-size: 16px;
+ color: var(--text-primary);
+ font-weight: 600;
+ font-size: 14px;
+ letter-spacing: -0.01em;
}
.uptime {
- color: var(--sw-text-secondary);
+ color: var(--text-tertiary);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
}
.content {
- padding: 15px;
+ padding: var(--space-5);
min-height: 400px;
+ background: var(--bg-primary);
}
.footer {
- border-top: 1px solid var(--sw-border-active);
- padding: 10px 15px;
+ border-top: 1px solid var(--border-default);
+ padding: var(--space-3) var(--space-5);
display: flex;
justify-content: space-between;
align-items: center;
- background: var(--sw-bg-header);
+ background: var(--bg-secondary);
font-size: 12px;
}
.refresh-info {
- color: var(--sw-text-secondary);
+ color: var(--text-tertiary);
+ font-size: 11px;
}
.status {
display: flex;
align-items: center;
- gap: 8px;
+ gap: var(--space-2);
}
.status-dot {
- width: 8px;
- height: 8px;
+ width: 6px;
+ height: 6px;
border-radius: 50%;
- background: var(--sw-text-primary);
- animation: pulse 2s infinite;
+ background: var(--accent-success);
+ }
+
+ .status-dot.offline {
+ background: var(--accent-error);
}
.prompt {
- color: var(--sw-text-primary);
- }
-
- .cursor {
- display: inline-block;
- width: 8px;
- height: 14px;
- background: var(--sw-text-primary);
- animation: blink 1s step-end infinite;
- vertical-align: middle;
- margin-left: 2px;
- }
-
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
- }
-
- @keyframes blink {
- 50% { opacity: 0; }
+ color: var(--text-secondary);
+ font-size: 11px;
}
`;
export const navStyles: CSSResult = css`
.nav {
display: flex;
- background: var(--sw-bg-header);
- border-bottom: 1px solid var(--sw-border);
- padding: 0 10px;
+ gap: var(--space-1);
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-default);
+ padding: var(--space-2) var(--space-3);
}
.nav-tab {
- padding: 10px 20px;
+ padding: var(--space-2) var(--space-4);
cursor: pointer;
- color: var(--sw-text-secondary);
+ color: var(--text-secondary);
border: none;
background: transparent;
font-family: inherit;
font-size: 13px;
- transition: all 0.2s;
- border-bottom: 2px solid transparent;
+ font-weight: 500;
+ transition: all 0.15s ease;
+ border-radius: var(--radius-sm);
}
.nav-tab:hover {
- color: var(--sw-text-primary);
+ color: var(--text-primary);
+ background: var(--bg-tertiary);
}
.nav-tab.active {
- color: var(--sw-text-primary);
- border-bottom-color: var(--sw-text-primary);
- background: var(--sw-bg-input);
+ color: var(--text-primary);
+ background: var(--bg-elevated);
}
.nav-tab .count {
- background: var(--sw-border);
- padding: 1px 6px;
- border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 18px;
+ background: var(--bg-tertiary);
+ padding: 0 6px;
+ border-radius: 9999px;
font-size: 11px;
- margin-left: 6px;
+ font-weight: 500;
+ margin-left: var(--space-2);
+ color: var(--text-secondary);
+ }
+
+ .nav-tab.active .count {
+ background: var(--accent-primary);
+ color: white;
}
`;
@@ -156,137 +191,167 @@ export const panelStyles: CSSResult = css`
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
- gap: 15px;
+ gap: var(--space-4);
}
.panel {
- border: 1px solid var(--sw-border);
- padding: 12px;
- background: var(--sw-bg-dark);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
}
.panel-title {
- color: var(--sw-text-cyan);
- font-weight: bold;
- margin-bottom: 10px;
- padding-bottom: 5px;
- border-bottom: 1px dashed var(--sw-border);
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: var(--space-3) var(--space-4);
+ border-bottom: 1px solid var(--border-default);
+ background: var(--bg-tertiary);
+ }
+
+ .panel-content {
+ padding: var(--space-4);
}
.row {
display: flex;
justify-content: space-between;
- padding: 3px 0;
+ align-items: center;
+ padding: var(--space-2) 0;
+ border-bottom: 1px solid var(--border-muted);
+ }
+
+ .row:last-child {
+ border-bottom: none;
}
.label {
- color: var(--sw-text-secondary);
+ color: var(--text-secondary);
+ font-size: 13px;
}
.value {
- color: var(--sw-text-primary);
+ color: var(--text-primary);
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
}
.value.warning {
- color: var(--sw-text-warning);
+ color: var(--accent-warning);
}
.value.error {
- color: var(--sw-text-error);
+ color: var(--accent-error);
}
.value.success {
- color: var(--sw-text-primary);
+ color: var(--accent-success);
}
`;
export const gaugeStyles: CSSResult = css`
.gauge {
- margin: 8px 0;
+ margin: var(--space-3) 0;
+ }
+
+ .gauge-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-2);
+ font-size: 12px;
+ }
+
+ .gauge-label {
+ color: var(--text-secondary);
+ }
+
+ .gauge-value {
+ color: var(--text-primary);
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
}
.gauge-bar {
- height: 16px;
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border);
- position: relative;
- font-size: 12px;
+ height: 6px;
+ background: var(--bg-tertiary);
+ border-radius: 3px;
+ overflow: hidden;
}
.gauge-fill {
height: 100%;
+ border-radius: 3px;
transition: width 0.3s ease;
}
.gauge-fill.good {
- background: var(--sw-gauge-good);
+ background: var(--accent-success);
}
.gauge-fill.warning {
- background: var(--sw-gauge-warning);
+ background: var(--accent-warning);
}
.gauge-fill.bad {
- background: var(--sw-gauge-bad);
- }
-
- .gauge-text {
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- color: #fff;
- font-weight: bold;
- text-shadow: 1px 1px 2px #000;
+ background: var(--accent-error);
}
`;
export const tableStyles: CSSResult = css`
.table-container {
overflow-x: auto;
+ border-radius: var(--radius-md);
}
.data-table {
width: 100%;
border-collapse: collapse;
- font-size: 12px;
- }
-
- .data-table th,
- .data-table td {
- padding: 8px 10px;
- text-align: left;
- border-bottom: 1px solid var(--sw-border);
+ font-size: 13px;
}
.data-table th {
- background: var(--sw-bg-input);
- color: var(--sw-text-cyan);
+ text-align: left;
+ padding: var(--space-3) var(--space-4);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+ font-weight: 500;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
cursor: pointer;
user-select: none;
white-space: nowrap;
+ border-bottom: 1px solid var(--border-default);
}
.data-table th:hover {
- background: #252525;
+ background: var(--bg-elevated);
+ color: var(--text-primary);
}
.data-table th .sort-icon {
- margin-left: 5px;
- opacity: 0.5;
+ margin-left: var(--space-1);
+ opacity: 0.4;
+ font-size: 10px;
}
.data-table th.sorted .sort-icon {
opacity: 1;
- color: var(--sw-text-primary);
- }
-
- .data-table tr:hover {
- background: #151515;
+ color: var(--accent-primary);
}
.data-table td {
- color: #ccc;
+ padding: var(--space-3) var(--space-4);
+ border-bottom: 1px solid var(--border-default);
+ color: var(--text-primary);
+ }
+
+ .data-table tr:hover td {
+ background: var(--bg-tertiary);
}
.data-table td.url {
@@ -294,83 +359,119 @@ export const tableStyles: CSSResult = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ color: var(--text-secondary);
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ font-size: 12px;
}
.data-table td.num {
text-align: right;
- color: var(--sw-text-primary);
+ font-variant-numeric: tabular-nums;
+ font-weight: 500;
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 10px;
- gap: 10px;
+ margin-bottom: var(--space-4);
+ gap: var(--space-3);
}
.search-input {
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border);
- color: var(--sw-text-primary);
- padding: 6px 10px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-default);
+ color: var(--text-primary);
+ padding: var(--space-2) var(--space-3);
font-family: inherit;
- font-size: 12px;
- width: 250px;
+ font-size: 13px;
+ width: 280px;
+ border-radius: var(--radius-md);
+ transition: all 0.15s ease;
+ }
+
+ .search-input::placeholder {
+ color: var(--text-tertiary);
}
.search-input:focus {
outline: none;
- border-color: var(--sw-border-active);
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.table-info {
- color: var(--sw-text-secondary);
+ color: var(--text-tertiary);
font-size: 12px;
}
.hit-rate-bar {
width: 60px;
- height: 10px;
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border);
+ height: 4px;
+ background: var(--bg-tertiary);
+ border-radius: 2px;
display: inline-block;
vertical-align: middle;
- margin-right: 6px;
+ margin-right: var(--space-2);
+ overflow: hidden;
}
.hit-rate-fill {
height: 100%;
+ border-radius: 2px;
}
.hit-rate-fill.good {
- background: var(--sw-gauge-good);
+ background: var(--accent-success);
}
.hit-rate-fill.warning {
- background: var(--sw-gauge-warning);
+ background: var(--accent-warning);
}
.hit-rate-fill.bad {
- background: var(--sw-gauge-bad);
+ background: var(--accent-error);
}
`;
export const buttonStyles: CSSResult = css`
.btn {
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border-active);
- color: var(--sw-text-primary);
- padding: 8px 16px;
- cursor: pointer;
+ padding: var(--space-2) var(--space-4);
+ border-radius: var(--radius-md);
font-family: inherit;
- font-size: 12px;
- transition: all 0.2s ease;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
}
- .btn:hover {
- background: var(--sw-text-primary);
- color: #000;
+ .btn-primary {
+ background: var(--accent-primary);
+ color: white;
+ border: none;
+ }
+
+ .btn-primary:hover {
+ background: #2563eb;
+ }
+
+ .btn-primary:active {
+ background: #1d4ed8;
+ }
+
+ .btn-secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border-default);
+ }
+
+ .btn-secondary:hover {
+ background: var(--bg-elevated);
+ border-color: var(--border-muted);
}
.btn:disabled {
@@ -381,114 +482,186 @@ export const buttonStyles: CSSResult = css`
.btn-row {
display: flex;
justify-content: flex-end;
- margin-top: 10px;
+ gap: var(--space-2);
+ margin-top: var(--space-4);
}
`;
export const speedtestStyles: CSSResult = css`
.online-indicator {
- display: flex;
+ display: inline-flex;
align-items: center;
- gap: 8px;
- padding: 8px 0;
- margin-bottom: 10px;
- border-bottom: 1px dashed var(--sw-border);
+ gap: var(--space-2);
+ padding: var(--space-1) var(--space-3);
+ border-radius: 9999px;
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: var(--space-4);
+ }
+
+ .online-indicator.online {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--accent-success);
+ }
+
+ .online-indicator.offline {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--accent-error);
}
.online-dot {
- width: 12px;
- height: 12px;
+ width: 6px;
+ height: 6px;
border-radius: 50%;
- transition: background-color 0.3s ease;
+ background: currentColor;
}
- .online-dot.online {
- background: var(--sw-text-primary);
- box-shadow: 0 0 8px rgba(0, 255, 0, 0.5);
+ .speedtest-results {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-4);
+ margin-top: var(--space-4);
}
- .online-dot.offline {
- background: var(--sw-text-error);
- box-shadow: 0 0 8px rgba(255, 68, 68, 0.5);
+ .speedtest-metric {
+ text-align: center;
+ padding: var(--space-4);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ }
+
+ .speedtest-value {
+ font-size: 24px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-primary);
+ line-height: 1.2;
+ }
+
+ .speedtest-unit {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-top: var(--space-1);
+ }
+
+ .speedtest-label {
+ font-size: 11px;
+ color: var(--text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-top: var(--space-2);
}
.speed-bar {
- height: 8px;
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border);
- margin: 4px 0;
+ height: 4px;
+ background: var(--bg-tertiary);
+ border-radius: 2px;
+ margin: var(--space-1) 0;
+ overflow: hidden;
}
.speed-fill {
height: 100%;
- background: var(--sw-gauge-good);
+ background: var(--accent-success);
+ border-radius: 2px;
transition: width 0.5s ease;
}
/* Speedtest progress indicator */
.speedtest-progress {
- padding: 10px 0;
+ padding: var(--space-4) 0;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 8px;
+ margin-bottom: var(--space-3);
}
.progress-phase {
- color: var(--sw-text-cyan);
- font-weight: bold;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--accent-info);
+ font-weight: 500;
+ font-size: 13px;
+ }
+
+ .progress-phase::before {
+ content: '';
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
animation: pulse 1s infinite;
}
.progress-time {
- color: var(--sw-text-secondary);
+ color: var(--text-tertiary);
font-size: 12px;
+ font-variant-numeric: tabular-nums;
}
.progress-bar {
- height: 20px;
- background: var(--sw-bg-input);
- border: 1px solid var(--sw-border);
- position: relative;
+ height: 6px;
+ background: var(--bg-tertiary);
+ border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
- background: linear-gradient(90deg, var(--sw-gauge-good), var(--sw-text-cyan));
+ background: var(--accent-info);
+ border-radius: 3px;
transition: width 0.1s linear;
- position: relative;
- }
-
- .progress-fill::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
- animation: shimmer 1.5s infinite;
}
.progress-fill.complete {
- background: var(--sw-text-primary);
- }
-
- .progress-fill.complete::after {
- display: none;
- }
-
- @keyframes shimmer {
- 0% { transform: translateX(-100%); }
- 100% { transform: translateX(100%); }
+ background: var(--accent-success);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
- 50% { opacity: 0.6; }
+ 50% { opacity: 0.4; }
+ }
+`;
+
+export const statusBadgeStyles: CSSResult = css`
+ .status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-1);
+ padding: var(--space-1) var(--space-2);
+ border-radius: 9999px;
+ font-size: 11px;
+ font-weight: 500;
+ }
+
+ .status-badge.success {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--accent-success);
+ }
+
+ .status-badge.warning {
+ background: rgba(234, 179, 8, 0.1);
+ color: var(--accent-warning);
+ }
+
+ .status-badge.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--accent-error);
+ }
+
+ .status-badge.info {
+ background: rgba(6, 182, 212, 0.1);
+ color: var(--accent-info);
+ }
+
+ .status-badge .badge-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
}
`;
diff --git a/ts_swdash/sw-dash-table.ts b/ts_swdash/sw-dash-table.ts
index 2bb9640..6624d68 100644
--- a/ts_swdash/sw-dash-table.ts
+++ b/ts_swdash/sw-dash-table.ts
@@ -151,7 +151,7 @@ export class SwDashTable extends LitElement {
>
${col.label}
${col.sortable !== false ? html`
-
${this.sortColumn === col.key && this.sortDirection === 'asc' ? '^' : 'v'}
+
${this.sortColumn === col.key && this.sortDirection === 'asc' ? '↑' : '↓'}
` : ''}
`)}