KManager / app /static /index.html
StarrySkyWorld's picture
feat: Add secret view
35e945c
Raw
History Blame Contribute Delete
289 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kiro Account Manager</title>
<style>
:root {
/* === Brand Colors === */
--accent: #48bb78;
--accent-hover: #68d391;
--accent-dim: rgba(72, 187, 120, 0.12);
--accent-glow: rgba(72, 187, 120, 0.4);
--danger: #f56565;
--danger-dim: rgba(245, 101, 101, 0.12);
--warning: #ecc94b;
--warning-dim: rgba(236, 201, 75, 0.12);
--success: #48bb78;
/* === Dark Theme (Glassmorphism) === */
--bg: #121212;
--bg-elevated: rgba(255, 255, 255, 0.04);
--bg-hover: rgba(255, 255, 255, 0.08);
--bg-active: rgba(72, 187, 120, 0.15);
--fg: #f0f0f0;
--fg-secondary: #b0b0b0;
--muted: #888888;
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.15);
--input-bg: rgba(255, 255, 255, 0.06);
--input-border: rgba(255, 255, 255, 0.12);
/* Glass effect */
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-blur: blur(12px);
/* === Fluid Typography (100px - 1000px) === */
--font-size-xs: clamp(9px, 1vw + 6px, 11px);
--font-size-sm: clamp(10px, 1.2vw + 6px, 12px);
--font-size-base: clamp(11px, 1.4vw + 6px, 13px);
--font-size-md: clamp(12px, 1.6vw + 6px, 14px);
--font-size-lg: clamp(14px, 2vw + 6px, 16px);
--font-size-xl: clamp(16px, 2.5vw + 6px, 20px);
--font-size-2xl: clamp(20px, 4vw + 6px, 28px);
--font-size-hero: clamp(24px, 8vw + 4px, 48px);
/* === Fluid Spacing (scales with viewport) === */
--space-1: clamp(2px, 0.4vw + 1px, 4px);
--space-2: clamp(4px, 0.8vw + 2px, 8px);
--space-3: clamp(6px, 1.2vw + 3px, 12px);
--space-4: clamp(8px, 1.6vw + 4px, 16px);
--space-5: clamp(10px, 2vw + 5px, 20px);
--space-6: clamp(12px, 2.4vw + 6px, 24px);
--space-8: clamp(16px, 3.2vw + 8px, 32px);
/* === Adaptive Padding (main container) === */
--padding-main: clamp(4px, 2vw, 16px);
--padding-card: clamp(6px, 2.5vw, 16px);
--padding-section: clamp(8px, 3vw, 20px);
/* === Touch-friendly sizes (fixed for usability) === */
--touch-target: 44px;
--button-min-height: 32px;
--input-min-height: 32px;
--toggle-width: 36px;
--toggle-height: 20px;
/* === Icon sizes (fluid) === */
--icon-xs: clamp(10px, 1.5vw + 6px, 14px);
--icon-sm: clamp(12px, 2vw + 6px, 16px);
--icon-md: clamp(14px, 2.5vw + 6px, 20px);
--icon-lg: clamp(18px, 3vw + 8px, 24px);
/* === Border Radius === */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* === Transitions === */
--transition-fast: 0.1s ease;
--transition: 0.15s ease;
--transition-normal: 0.2s ease;
--transition-slow: 0.3s ease;
/* === Shadows === */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 20px rgba(72, 187, 120, 0.3);
/* === Z-index layers === */
--z-base: 1;
--z-dropdown: 10;
--z-sticky: 20;
--z-drawer: 50;
--z-fab: 80;
--z-overlay: 100;
--z-modal: 200;
--z-dialog: 300;
--z-toast: 400;
/* === Grid breakpoints (for JS detection) === */
--bp-narrow: 250px;
--bp-medium: 500px;
--bp-wide: 750px;
}
/* === Reset === */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
overflow: hidden;
background: var(--bg);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: var(--font-size-base);
line-height: 1.5;
color: var(--fg);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* === App Container === */
.app {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
min-width: 100px;
max-width: 100%;
overflow: hidden;
}
/* === Scrollbars (thin, dark, rounded) === */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 3px;
border: 1px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active {
background: var(--accent);
background-clip: padding-box;
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
}
/* List scrollbar - accent tinted */
.list::-webkit-scrollbar-thumb {
background: rgba(72, 187, 120, 0.2);
}
.list::-webkit-scrollbar-thumb:hover {
background: rgba(72, 187, 120, 0.35);
}
/* Console scrollbar */
.console-body::-webkit-scrollbar {
width: 4px;
}
.console-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
}
/* === Animations === */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 4px var(--accent-glow); }
50% { box-shadow: 0 0 12px var(--accent-glow); }
}
.animate-fade-in { animation: fadeIn 0.2s ease-out; }
.animate-slide-in { animation: slideIn 0.2s ease-out; }
.animate-pulse { animation: pulse 1.5s ease-in-out infinite; }
/* === Text Utilities === */
.hidden { display: none !important; }
.text-muted { color: var(--muted); }
.text-accent { color: var(--accent); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-mono { font-family: 'JetBrains Mono', 'Fira Code', monospace; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* === Spacing Utilities === */
.m-0 { margin: 0; }
.m-1 { margin: var(--space-1); }
.m-2 { margin: var(--space-2); }
.m-3 { margin: var(--space-3); }
.m-4 { margin: var(--space-4); }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.p-0 { padding: 0; }
.p-1 { padding: var(--space-1); }
.p-2 { padding: var(--space-2); }
.p-3 { padding: var(--space-3); }
.p-4 { padding: var(--space-4); }
.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); }
.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); }
.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); }
.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
/* === Flex Utilities === */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1; min-width: 0; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
/* === Width/Height === */
.w-full { width: 100%; }
.h-full { height: 100%; }
.min-w-0 { min-width: 0; }
/* === Border Utilities === */
.rounded-sm { border-radius: var(--radius-sm); }
.rounded-md { border-radius: var(--radius-md); }
.rounded-lg { border-radius: var(--radius-lg); }
.rounded-full { border-radius: var(--radius-full); }
/* === Overflow === */
.overflow-hidden { overflow: hidden; }
.overflow-auto { overflow: auto; }
.overflow-y-auto { overflow-y: auto; overflow-x: hidden; }
/* === Cursor === */
.cursor-pointer { cursor: pointer; }
.cursor-default { cursor: default; }
/* === Transitions === */
.transition { transition: all var(--transition); }
.transition-colors {
transition: color var(--transition),
background-color var(--transition),
border-color var(--transition);
}
/* === Buttons === */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
min-height: var(--button-min-height);
font-size: var(--font-size-sm);
font-family: inherit;
font-weight: 600;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: background-color var(--transition),
color var(--transition),
border-color var(--transition),
box-shadow var(--transition),
transform var(--transition-fast);
white-space: nowrap;
position: relative;
overflow: hidden;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.btn:active:not(:disabled) {
transform: translateY(0) scale(0.98);
}
.btn:focus { outline: none; }
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (hover: none) {
.btn:hover:not(:disabled) {
transform: none;
}
}
.btn svg {
width: var(--icon-sm);
height: var(--icon-sm);
flex-shrink: 0;
}
/* Button text - hide on narrow screens */
.btn .btn-text {
display: inline;
}
/* Ultra-narrow: icon-only buttons */
@media (max-width: 200px) {
.btn .btn-text { display: none; }
.btn {
padding: var(--space-2);
min-width: var(--button-min-height);
max-width: var(--button-min-height);
}
}
@media (max-width: 250px) {
.btn .btn-text { display: none; }
.btn { padding: var(--space-2); min-width: var(--button-min-height); }
}
/* Wide view: constrain button width */
@media (min-width: 500px) {
.btn:not(.btn-block) {
max-width: 200px;
}
}
.btn-primary {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
color: #fff;
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.3);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 4px 16px rgba(72, 187, 120, 0.4);
}
.btn-secondary {
background: var(--glass-bg);
color: var(--fg);
border: 1px solid var(--glass-border);
backdrop-filter: var(--glass-blur);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-strong);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger) 0%, #fc8181 100%);
color: #fff;
box-shadow: 0 2px 8px rgba(245, 101, 101, 0.3);
}
.btn-danger:hover:not(:disabled) {
box-shadow: 0 4px 16px rgba(245, 101, 101, 0.4);
}
.btn-warning {
background: linear-gradient(135deg, var(--warning) 0%, #f6e05e 100%);
color: #1a1a1a;
box-shadow: 0 2px 8px rgba(236, 201, 75, 0.3);
}
.btn-warning:hover:not(:disabled) {
box-shadow: 0 4px 16px rgba(236, 201, 75, 0.4);
}
.btn-ghost {
background: transparent;
color: var(--muted);
border: 1px solid transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--fg);
}
.btn-sm {
padding: var(--space-1) var(--space-2);
min-height: 28px;
font-size: var(--font-size-xs);
}
.btn-lg {
padding: var(--space-3) var(--space-5);
min-height: 40px;
font-size: var(--font-size-md);
}
.btn-block {
width: 100%;
}
.btn-full {
width: 100%;
}
/* Icon-only button variant */
.btn.btn-icon {
padding: var(--space-2);
min-width: var(--button-min-height);
width: var(--button-min-height);
max-width: var(--button-min-height);
}
.btn.btn-icon svg {
width: var(--icon-sm);
height: var(--icon-sm);
}
/* === Icon Button === */
.icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: var(--button-min-height);
height: var(--button-min-height);
min-width: var(--button-min-height);
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--muted);
transition: all var(--transition);
}
.icon-btn:hover {
background: var(--bg-hover);
border-color: var(--border);
color: var(--fg);
}
.icon-btn:active {
transform: scale(0.95);
}
.icon-btn svg {
width: var(--icon-sm);
height: var(--icon-sm);
}
.icon-btn.active {
background: var(--accent-dim);
color: var(--accent);
border-color: var(--accent);
}
/* === Inputs === */
.form-group {
margin-bottom: var(--space-3);
width: 100%;
}
.form-group:last-child { margin-bottom: 0; }
.form-label {
display: block;
font-size: var(--font-size-xs);
font-weight: 600;
margin-bottom: var(--space-1);
color: var(--fg);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-input {
width: 100%;
padding: var(--space-2) var(--space-3);
min-height: var(--input-min-height);
font-size: var(--font-size-sm);
font-family: inherit;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
outline: none;
transition: all var(--transition);
}
.form-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.form-input::placeholder {
color: var(--muted);
}
.form-hint {
font-size: var(--font-size-xs);
color: var(--muted);
margin-top: var(--space-1);
}
.form-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.form-row > * {
flex: 1;
min-width: 80px;
}
/* === Toggle (fixed size for touch) === */
.toggle {
position: relative;
display: inline-block;
width: var(--toggle-width);
height: var(--toggle-height);
flex-shrink: 0;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: rgba(255, 255, 255, 0.15);
border-radius: var(--toggle-height);
transition: all var(--transition);
}
.toggle-slider::before {
content: '';
position: absolute;
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background: #fff;
border-radius: 50%;
transition: all var(--transition);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.toggle input:checked + .toggle-slider {
background: var(--accent);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(16px);
}
/* === Select === */
.select {
width: 100%;
padding: var(--space-2) var(--space-6) var(--space-2) var(--space-3);
min-height: var(--input-min-height);
font-size: var(--font-size-sm);
font-family: inherit;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
cursor: pointer;
appearance: none;
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='%23888' d='M6 8L2 4h8z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--space-3) center;
transition: all var(--transition);
}
.select:hover {
border-color: var(--accent);
}
.select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
/* === Number Input === */
.input-number {
width: 70px;
padding: var(--space-2);
min-height: var(--input-min-height);
font-size: var(--font-size-sm);
font-family: inherit;
font-weight: 600;
text-align: center;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
transition: all var(--transition);
}
.input-number:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
/* === Card === */
.card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: var(--padding-card);
transition: all var(--transition);
}
.card:hover {
border-color: var(--border-strong);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-3);
}
.card-title {
font-size: var(--font-size-md);
font-weight: 600;
}
/* === Badge === */
.badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
font-weight: 600;
border-radius: var(--radius-full);
white-space: nowrap;
}
.badge-success {
background: var(--accent-dim);
color: var(--accent);
}
.badge-warning {
background: var(--warning-dim);
color: var(--warning);
}
.badge-danger {
background: var(--danger-dim);
color: var(--danger);
}
/* === Spinner === */
.spinner {
width: var(--icon-sm);
height: var(--icon-sm);
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* === Tooltip === */
.tooltip {
position: relative;
}
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
font-weight: 500;
white-space: nowrap;
color: #fff;
background: #2a2a2a;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
z-index: var(--z-toast);
}
.tooltip:hover::after {
opacity: 1;
visibility: visible;
transition-delay: 0.3s, 0.3s;
}
/* Tooltip for icon-only buttons */
.icon-btn[data-tooltip]::after,
.account-btn[data-tooltip]::after {
font-size: 10px;
padding: 4px 8px;
}
/* === Button Group (equal width) === */
.btn-group {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.btn-group .btn {
flex: 1;
min-width: 80px;
}
/* Stack vertically on narrow */
@media (max-width: 200px) {
.btn-group {
flex-direction: column;
}
.btn-group .btn {
width: 100%;
max-width: none;
}
}
/* === Focus States === */
.form-input:focus,
.form-control:focus,
.select:focus,
.input-number:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim);
}
.btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* === Header Container === */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-2) var(--padding-main);
background: var(--bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
min-height: 40px;
gap: var(--space-2);
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
flex: 1;
}
.header-title {
font-size: var(--font-size-md);
font-weight: 700;
color: var(--fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-badge {
font-size: var(--font-size-xs);
font-weight: 600;
padding: 2px var(--space-2);
background: var(--accent-dim);
color: var(--accent);
border-radius: var(--radius-full);
flex-shrink: 0;
}
.header-actions {
display: flex;
gap: var(--space-1);
flex-shrink: 0;
}
/* === Narrow: hide text, show icons only === */
@media (max-width: 250px) {
.header {
padding: var(--space-1) var(--space-2);
min-height: 36px;
}
.header-title {
font-size: var(--font-size-sm);
max-width: 60px;
}
.header-badge { display: none; }
.header-actions .btn-text { display: none; }
}
/* === Medium: standard sidebar === */
@media (min-width: 251px) and (max-width: 500px) {
.header-title { max-width: 120px; }
}
/* === Wide: full layout === */
@media (min-width: 501px) {
.header {
padding: var(--space-3) var(--space-4);
}
.header-title {
font-size: var(--font-size-lg);
max-width: none;
}
}
/* === Hero Dashboard === */
.hero {
margin: var(--space-2) var(--padding-main);
padding: var(--padding-card);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, rgba(72, 187, 120, 0.02) 100%);
border: 1px solid rgba(72, 187, 120, 0.25);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-normal);
flex-shrink: 0;
}
.hero:hover {
border-color: rgba(72, 187, 120, 0.4);
box-shadow: var(--shadow-glow);
transform: translateY(-1px);
}
.hero.empty {
background: var(--glass-bg);
border-color: var(--glass-border);
text-align: center;
}
.hero.warning {
background: linear-gradient(135deg, rgba(236, 201, 75, 0.12) 0%, rgba(236, 201, 75, 0.02) 100%);
border-color: rgba(236, 201, 75, 0.4);
}
.hero.critical {
background: linear-gradient(135deg, rgba(245, 101, 101, 0.12) 0%, rgba(245, 101, 101, 0.02) 100%);
border-color: rgba(245, 101, 101, 0.4);
animation: criticalPulse 2s ease-in-out infinite;
}
@keyframes criticalPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 101, 101, 0.3); }
50% { box-shadow: 0 0 0 4px rgba(245, 101, 101, 0.1); }
}
/* === Hero Main Value === */
.hero-main {
text-align: center;
padding: var(--space-2) 0;
}
.hero-value {
display: block;
font-size: var(--font-size-hero);
font-weight: 700;
line-height: 1;
letter-spacing: -1px;
color: var(--accent);
}
.hero-value.low { color: var(--accent); }
.hero-value.medium { color: var(--warning); }
.hero-value.high { color: var(--danger); }
.hero-label {
display: block;
font-size: var(--font-size-xs);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: var(--space-2);
}
/* === Progress Bar (100% width) === */
.hero-progress {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
margin-top: var(--space-3);
}
.hero-progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.4s ease;
}
.hero-progress-fill.low {
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
}
.hero-progress-fill.medium {
background: linear-gradient(90deg, var(--warning), #f6e05e);
}
.hero-progress-fill.high {
background: linear-gradient(90deg, var(--danger), #fc8181);
}
/* === Hero Footer Stats - Two Column Layout === */
.hero-footer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
font-size: var(--font-size-xs);
color: var(--muted);
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.hero-stat {
display: flex;
align-items: center;
gap: var(--space-2);
}
.hero-stat-icon {
font-size: var(--font-size-sm);
opacity: 0.7;
}
.hero-stat-value {
font-weight: 600;
color: var(--fg);
}
.hero-stat-label {
color: var(--muted);
font-size: 10px;
}
.hero-percent {
font-weight: 700;
color: var(--accent);
}
/* === Step Indicators === */
.step-indicators {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
margin: var(--space-3) 0;
padding: var(--space-3) var(--space-2);
background: var(--bg-elevated);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
}
.step-indicator {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
min-width: 32px;
}
.step-icon {
font-size: var(--font-size-md);
opacity: 0.4;
transition: all 0.3s ease;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
opacity: 0.3;
transition: all 0.3s ease;
}
.step-line {
flex: 1;
height: 2px;
background: var(--muted);
opacity: 0.2;
min-width: 8px;
max-width: 24px;
}
.step-indicator.done .step-icon { opacity: 1; }
.step-indicator.done .step-dot {
background: var(--accent);
opacity: 1;
box-shadow: 0 0 8px var(--accent-glow);
}
.step-indicator.active .step-icon {
opacity: 1;
animation: stepPulse 1s ease-in-out infinite;
}
.step-indicator.active .step-dot {
background: var(--accent);
opacity: 1;
animation: glow 1s ease-in-out infinite;
}
.step-indicator.error .step-dot {
background: var(--danger);
opacity: 1;
}
@keyframes stepPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* === Narrow: compact hero === */
@media (max-width: 250px) {
.hero {
margin: var(--space-1);
padding: var(--space-2);
}
.hero-value { font-size: clamp(20px, 12vw, 28px); }
.hero-label { font-size: 9px; letter-spacing: 0.5px; }
.hero-footer { display: none; }
.step-indicators { display: none; }
}
/* === Wide: expanded hero === */
@media (min-width: 501px) {
.hero {
margin: var(--space-3) var(--space-4);
padding: var(--space-5);
}
.hero-value { font-size: clamp(32px, 6vw, 48px); }
.hero-progress { height: 8px; }
}
/* === Toolbar Container === */
.toolbar {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-2) var(--padding-main);
border-bottom: 1px solid var(--border);
background: var(--bg-elevated);
flex-shrink: 0;
}
.toolbar-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
}
.toolbar-buttons {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.toolbar-primary {
display: flex;
gap: 2px;
}
.toolbar-secondary {
display: flex;
gap: 2px;
}
/* Hide secondary buttons on narrow, show in dropdown */
@media (max-width: 350px) {
.toolbar-secondary {
display: none;
}
}
@media (min-width: 351px) {
.toolbar-more-wrapper {
display: none;
}
}
/* === Search === */
.search-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 80px;
}
.search-input {
width: 100%;
padding: var(--space-2) var(--space-3);
padding-left: var(--space-8);
font-size: var(--font-size-sm);
font-family: inherit;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
transition: all var(--transition);
}
.search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.search-input::placeholder {
color: var(--muted);
}
.search-icon {
position: absolute;
left: var(--space-3);
top: 50%;
transform: translateY(-50%);
color: var(--muted);
pointer-events: none;
}
.search-icon svg {
width: var(--icon-sm);
height: var(--icon-sm);
}
.search-clear {
position: absolute;
right: var(--space-2);
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
display: none;
align-items: center;
justify-content: center;
background: var(--bg-elevated);
border: none;
border-radius: 50%;
cursor: pointer;
color: var(--muted);
font-size: var(--font-size-xs);
transition: all var(--transition);
}
.search-clear:hover {
background: var(--danger-dim);
color: var(--danger);
}
.search-wrapper:has(.search-input:not(:placeholder-shown)) .search-clear {
display: flex;
}
/* === Filters === */
.filter-group {
display: flex;
align-items: center;
gap: var(--space-1);
flex-wrap: wrap;
}
.filter-label {
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--muted);
margin-right: var(--space-1);
flex-shrink: 0;
}
.filter-btn {
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: inherit;
background: var(--bg-elevated);
color: var(--muted);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition);
white-space: nowrap;
}
.filter-btn:hover {
background: var(--bg-hover);
border-color: var(--accent);
color: var(--fg);
}
.filter-btn.active {
background: var(--accent-dim);
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.filter-select {
padding: var(--space-1) var(--space-5) var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
font-family: inherit;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%23888' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--space-2) center;
transition: all var(--transition);
}
.filter-select:hover {
border-color: var(--accent);
}
/* === Bulk Actions === */
.bulk-actions-bar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: linear-gradient(135deg, var(--accent-dim) 0%, rgba(72, 187, 120, 0.02) 100%);
border-radius: var(--radius-md);
}
.bulk-actions-bar.hidden {
display: none;
}
.bulk-info {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--font-size-xs);
color: var(--accent);
font-weight: 600;
}
.bulk-count {
background: var(--accent);
color: #fff;
padding: 2px 6px;
border-radius: var(--radius-full);
font-size: 10px;
min-width: 18px;
text-align: center;
}
.bulk-buttons {
display: flex;
gap: var(--space-1);
flex: 1;
flex-wrap: wrap;
}
/* === More Menu (Dropdown) === */
.toolbar-more-wrapper {
position: relative;
}
.toolbar-more-btn {
width: 32px;
height: 32px;
min-width: 32px;
}
.toolbar-more-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 160px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: var(--z-dropdown);
display: none;
flex-direction: column;
padding: var(--space-1);
animation: fadeIn 0.15s ease;
}
.toolbar-more-menu.visible {
display: flex;
}
.toolbar-more-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-sm);
font-family: inherit;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--fg);
cursor: pointer;
transition: all var(--transition);
text-align: left;
white-space: nowrap;
}
.toolbar-more-item:hover {
background: var(--bg-hover);
}
.toolbar-more-item .more-icon {
font-size: var(--font-size-md);
width: 20px;
text-align: center;
}
.toolbar-more-item .more-label {
flex: 1;
}
.toolbar-more-divider {
height: 1px;
background: var(--border);
margin: var(--space-1) 0;
}
/* === Narrow: compact toolbar === */
@media (max-width: 250px) {
.toolbar {
padding: var(--space-1);
gap: var(--space-1);
}
.toolbar-row { gap: var(--space-1); }
.filter-label { display: none; }
.filter-btn {
padding: var(--space-1);
font-size: 9px;
}
.search-input {
padding: var(--space-1) var(--space-2);
padding-left: var(--space-6);
font-size: var(--font-size-xs);
}
}
/* === Wide: horizontal layout === */
@media (min-width: 500px) {
.toolbar {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.toolbar-row {
flex: 1;
}
}
/* === Account List Container === */
.list {
flex: 1;
overflow-y: auto;
padding: var(--space-2) var(--padding-main);
padding-bottom: 120px; /* Space for console drawer */
}
/* === Grid Layout for Wide Screens === */
@media (min-width: 500px) {
.list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--space-3);
align-content: start;
}
.list-group {
grid-column: 1 / -1;
}
}
/* === List Group Header === */
.list-group {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-1);
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-left: 3px solid transparent;
margin-left: -3px;
padding-left: calc(var(--space-1) + 3px);
}
/* Status color markers */
.list-group.active-group {
color: var(--accent);
border-left-color: var(--accent);
background: linear-gradient(90deg, rgba(72, 187, 120, 0.08) 0%, transparent 50%);
}
.list-group.ready-group {
color: var(--muted);
border-left-color: #718096;
}
.list-group.danger {
color: var(--danger);
border-left-color: var(--danger);
background: linear-gradient(90deg, rgba(245, 101, 101, 0.08) 0%, transparent 50%);
}
.list-group.warning {
color: var(--warning);
border-left-color: var(--warning);
background: linear-gradient(90deg, rgba(236, 201, 75, 0.08) 0%, transparent 50%);
}
.list-group.expired-group {
color: var(--warning);
border-left-color: var(--warning);
background: linear-gradient(90deg, rgba(236, 201, 75, 0.08) 0%, transparent 50%);
}
.list-group-count {
padding: 2px var(--space-2);
background: var(--bg-elevated);
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
}
.list-group-action {
margin-left: auto;
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
font-weight: 600;
background: var(--danger-dim);
color: var(--danger);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition);
}
.list-group-action:hover {
background: var(--danger);
color: #fff;
}
/* === Account Card === */
.account {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-3);
margin-bottom: var(--space-2);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
min-width: 0;
}
.account:hover {
border-color: rgba(72, 187, 120, 0.5);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, rgba(72, 187, 120, 0.02) 100%);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(72, 187, 120, 0.2);
}
.account:active {
transform: scale(0.99);
}
/* === Account States */
.account.active {
border-color: var(--accent);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.12) 0%, rgba(72, 187, 120, 0.02) 100%);
box-shadow: 0 0 12px var(--accent-glow);
}
.account.switching {
pointer-events: none;
opacity: 0.7;
position: relative;
}
.account.switching::after {
content: '';
position: absolute;
top: 50%;
right: 16px;
width: 20px;
height: 20px;
margin-top: -10px;
border: 2px solid var(--accent);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.account.expired {
border-color: var(--warning);
background: linear-gradient(135deg, rgba(236, 201, 75, 0.08) 0%, transparent 100%);
}
.account.exhausted {
border-color: #ed8936;
background: linear-gradient(135deg, rgba(237, 137, 54, 0.08) 0%, transparent 100%);
}
.account.banned {
border-color: var(--danger);
background: linear-gradient(135deg, rgba(245, 101, 101, 0.12) 0%, transparent 100%);
opacity: 0.7;
}
.account.banned::after {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: var(--danger);
}
/* === Account Avatar === */
.account-avatar {
position: relative;
width: clamp(28px, 8vw, 36px);
height: clamp(28px, 8vw, 36px);
border-radius: 50%;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.account.expired .account-avatar {
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
}
.account.banned .account-avatar {
background: linear-gradient(135deg, var(--danger) 0%, #fc8181 100%);
}
.account-status {
position: absolute;
bottom: -2px;
right: -2px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--bg);
}
.account-status.active {
background: var(--accent);
box-shadow: 0 0 6px var(--accent);
}
.account-status.ready { background: #718096; }
.account-status.expired { background: var(--warning); }
.account-status.banned { background: var(--danger); }
/* === Account Info === */
.account-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.account-email {
font-size: var(--font-size-sm);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.account-meta {
display: flex;
gap: var(--space-2);
margin-top: 2px;
font-size: var(--font-size-xs);
color: var(--muted);
flex-wrap: wrap;
}
.account-meta span {
display: flex;
align-items: center;
gap: var(--space-1);
white-space: nowrap;
}
.account-meta svg {
width: var(--icon-xs);
height: var(--icon-xs);
}
/* === Account Actions === */
.account-actions {
display: flex;
gap: var(--space-1);
flex-shrink: 0;
opacity: 0.7;
transition: opacity var(--transition);
}
.account:hover .account-actions { opacity: 1; }
/* Larger hit area for action buttons */
.account-btn {
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
color: var(--muted);
transition: all var(--transition);
padding: 0;
}
.account-btn:hover {
background: var(--bg-hover);
color: var(--fg);
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.account-btn:active {
transform: scale(0.95);
}
.account-btn.danger:hover {
background: var(--danger-dim);
border-color: var(--danger);
color: var(--danger);
}
.account-btn.danger.confirm-delete {
background: linear-gradient(135deg, var(--danger) 0%, #fc8181 100%);
border-color: var(--danger);
color: #fff;
box-shadow: 0 0 0 2px rgba(245, 101, 101, 0.25), 0 6px 18px rgba(245, 101, 101, 0.25);
animation: confirmDeletePulse 1s ease-in-out infinite;
}
.account-btn.danger.confirm-delete:hover {
transform: scale(1.12);
box-shadow: 0 0 0 2px rgba(245, 101, 101, 0.35), 0 10px 22px rgba(245, 101, 101, 0.35);
}
@keyframes confirmDeletePulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.08); }
}
.account-btn svg {
width: 14px;
height: 14px;
}
/* Touch devices: even larger buttons */
@media (hover: none) {
.account-btn {
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
}
.account-btn svg {
width: 18px;
height: 18px;
}
}
/* === Empty State === */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--space-8) var(--space-4);
color: var(--muted);
}
.empty-illustration {
margin-bottom: var(--space-4);
opacity: 0.8;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.empty-title {
font-size: var(--font-size-md);
font-weight: 600;
color: var(--fg);
margin-bottom: var(--space-2);
}
.empty-desc {
font-size: var(--font-size-sm);
color: var(--muted);
max-width: 220px;
line-height: 1.5;
margin-bottom: var(--space-4);
}
/* === Narrow: stack actions below === */
@media (max-width: 200px) {
.account {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
.account-actions {
justify-content: flex-end;
opacity: 1;
}
}
/* === Touch devices: always show actions === */
@media (hover: none) {
.account-actions { opacity: 1 !important; }
}
/* === Wide: grid mode === */
@media (min-width: 500px) {
.account {
margin-bottom: 0;
}
}
/* === Overlay (Full Screen) === */
.overlay {
position: fixed;
inset: 0;
background: var(--bg);
z-index: var(--z-overlay);
display: none;
flex-direction: column;
animation: slideIn 0.25s ease;
}
.overlay.visible { display: flex; }
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.overlay-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--padding-main);
background: linear-gradient(180deg, var(--bg-elevated) 0%, transparent 100%);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.overlay-back {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
font-weight: 600;
font-family: inherit;
background: transparent;
border: none;
color: var(--accent);
cursor: pointer;
transition: all var(--transition);
}
.overlay-back:hover {
color: var(--accent-hover);
}
.overlay-title {
font-size: var(--font-size-md);
font-weight: 600;
flex: 1;
}
.overlay-content {
flex: 1;
overflow-y: auto;
padding: var(--padding-section);
}
.overlay-footer {
padding: var(--space-3) var(--padding-main);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-elevated);
flex-shrink: 0;
}
.overlay-version {
font-size: var(--font-size-xs);
color: var(--muted);
}
/* === Modal === */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: var(--z-modal);
display: none;
align-items: center;
justify-content: center;
padding: var(--space-4);
animation: fadeIn 0.15s ease;
}
.modal-overlay.visible { display: flex; }
.modal {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
width: 100%;
max-width: 400px;
max-height: 90vh;
overflow: hidden;
box-shadow: var(--shadow-lg);
animation: modalSlideIn 0.2s ease;
}
@keyframes modalSlideIn {
from { opacity: 0; transform: scale(0.95) translateY(-10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
}
.modal-title {
font-size: var(--font-size-md);
font-weight: 600;
}
.modal-close {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--muted);
cursor: pointer;
font-size: var(--font-size-lg);
border-radius: var(--radius-sm);
transition: all var(--transition);
}
.modal-close:hover {
background: var(--danger-dim);
color: var(--danger);
}
.modal-body {
padding: var(--space-4);
overflow-y: auto;
max-height: 60vh;
}
.modal-hint {
font-size: var(--font-size-xs);
color: var(--muted);
line-height: 1.6;
margin-bottom: var(--space-3);
}
.modal-textarea {
width: 100%;
min-height: 80px;
padding: var(--space-3);
font-size: var(--font-size-xs);
font-family: 'JetBrains Mono', monospace;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
resize: vertical;
margin-bottom: var(--space-3);
}
.modal-textarea:focus {
outline: none;
border-color: var(--accent);
}
.modal-footer {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border);
background: var(--bg-elevated);
}
/* === Dialog (Confirmation) === */
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: var(--z-dialog);
display: none;
align-items: center;
justify-content: center;
padding: var(--space-4);
}
.dialog-overlay.visible { display: flex; }
.dialog {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-5);
width: 100%;
max-width: 320px;
box-shadow: var(--shadow-lg);
animation: modalSlideIn 0.2s ease;
}
.dialog-title {
font-size: var(--font-size-md);
font-weight: 600;
margin-bottom: var(--space-2);
}
.dialog-text {
font-size: var(--font-size-sm);
color: var(--muted);
margin-bottom: var(--space-4);
line-height: 1.5;
}
.dialog-actions {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
}
/* === Narrow: full-width modals === */
@media (max-width: 250px) {
.modal, .dialog {
max-width: none;
border-radius: 0;
max-height: 100vh;
}
.modal-body {
max-height: calc(100vh - 120px);
}
}
/* === Settings Container === */
.settings-content {
padding: var(--padding-section);
overflow-y: auto;
flex: 1;
}
/* === Setting Row === */
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--border);
gap: var(--space-3);
}
.setting-row:last-child {
border-bottom: none;
}
.setting-info {
flex: 1;
min-width: 0;
}
.setting-label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--fg);
}
.setting-desc {
font-size: var(--font-size-xs);
color: var(--muted);
margin-top: 2px;
}
/* === Settings Card (Accordion) === */
.settings-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-3);
overflow: hidden;
transition: all var(--transition);
}
.settings-card-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: rgba(255, 255, 255, 0.02);
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
width: 100%; /* Full width clickable */
transition: background var(--transition);
}
.settings-card-header:hover {
background: var(--bg-hover);
}
.settings-card-icon {
font-size: var(--font-size-lg);
display: flex;
align-items: center;
justify-content: center;
width: var(--space-6);
height: var(--space-6);
background: var(--bg-hover);
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.settings-card-title {
font-size: var(--font-size-sm);
font-weight: 600;
flex: 1;
color: var(--fg);
}
.settings-card-toggle {
transition: transform 0.2s ease;
color: var(--muted);
font-size: var(--font-size-xs);
flex-shrink: 0;
}
.settings-card.collapsed .settings-card-toggle {
transform: rotate(-90deg);
}
.settings-card.collapsed .settings-card-body {
display: none;
}
.settings-card-body {
padding: var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
/* === Import/Export Section === */
.import-export-section {
margin-top: var(--space-4);
padding: var(--space-4);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.06) 0%, transparent 100%);
border: 1px solid rgba(72, 187, 120, 0.2);
border-radius: var(--radius-lg);
}
.import-export-section .section-header {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.import-export-section .section-icon {
font-size: var(--font-size-md);
}
.import-export-section .section-title {
font-size: var(--font-size-sm);
font-weight: 600;
}
.import-export-section .section-desc {
font-size: var(--font-size-xs);
color: var(--muted);
margin-bottom: var(--space-3);
}
.import-export-actions {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.import-export-actions .btn {
flex: 1;
min-width: 100px;
}
/* === Danger Zone === */
.danger-zone-section {
margin-top: var(--space-6);
padding-top: var(--space-4);
border-top: 1px dashed rgba(245, 101, 101, 0.3);
}
.danger-zone-header {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.danger-zone-icon {
font-size: var(--font-size-md);
}
.danger-zone-title {
font-size: var(--font-size-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--danger);
}
.danger-zone-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
background: linear-gradient(135deg, rgba(245, 101, 101, 0.06) 0%, transparent 100%);
border: 1px solid rgba(245, 101, 101, 0.2);
border-radius: var(--radius-lg);
gap: var(--space-3);
flex-wrap: wrap;
}
.danger-zone-card:hover {
border-color: rgba(245, 101, 101, 0.4);
}
.danger-zone-info {
flex: 1;
min-width: 120px;
}
.danger-zone-label {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--fg);
margin-bottom: var(--space-1);
}
.danger-zone-desc {
font-size: var(--font-size-xs);
color: var(--muted);
line-height: 1.4;
}
.danger-zone-actions {
display: flex;
gap: var(--space-2);
flex-shrink: 0;
}
.danger-zone-card + .danger-zone-card {
margin-top: var(--space-3);
}
/* === Patch Status === */
.patch-status-row {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
flex-wrap: wrap;
}
.patch-status {
font-size: var(--font-size-xs);
font-weight: 600;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
}
.patch-status.success {
color: var(--accent);
background: var(--accent-dim);
}
.patch-status.warning {
color: var(--warning);
background: var(--warning-dim);
}
.patch-status.error {
color: var(--danger);
background: var(--danger-dim);
}
/* === Settings Footer === */
.settings-footer {
padding: var(--space-3);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
.settings-version {
font-size: var(--font-size-xs);
color: var(--muted);
}
/* === Wide: multi-column settings === */
@media (min-width: 600px) {
.settings-card-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.setting-row {
grid-column: span 1;
}
.setting-row.full-width {
grid-column: 1 / -1;
}
}
/* === Console Drawer === */
.console-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #0d0d0d;
border-top: 1px solid var(--border);
z-index: var(--z-drawer);
transform: translateY(calc(100% - 32px));
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
min-height: 32px;
max-height: 60vh;
}
.console-drawer.has-errors {
border-top-color: var(--danger);
box-shadow: 0 -4px 20px rgba(245, 101, 101, 0.2);
}
.console-drawer.has-warnings {
border-top-color: var(--warning);
}
.console-drawer.open {
transform: translateY(0);
}
/* === Console Header === */
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
cursor: pointer;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, transparent 100%);
border-bottom: 1px solid var(--border);
user-select: none;
min-height: 32px;
transition: background var(--transition);
}
.console-header:hover {
background: rgba(255, 255, 255, 0.04);
}
.console-header-left {
display: flex;
align-items: center;
gap: var(--space-2);
}
.console-header-right {
display: flex;
align-items: center;
gap: var(--space-1);
}
.console-title-group {
display: flex;
align-items: center;
gap: var(--space-2);
}
.console-icon-indicator {
font-size: 6px;
color: var(--muted);
opacity: 0.5;
transition: all 0.3s ease;
}
.console-icon-indicator.has-errors {
color: var(--danger);
opacity: 1;
animation: pulse 1.5s ease-in-out infinite;
}
.console-icon-indicator.has-warnings {
color: var(--warning);
opacity: 1;
}
.console-title {
font-size: var(--font-size-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--muted);
}
/* Hide "КОНСОЛЬ" text on narrow, show only badge */
@media (max-width: 200px) {
.console-title { display: none; }
}
.console-badge {
font-size: 9px;
padding: 1px 6px;
background: var(--bg-elevated);
color: var(--muted);
border-radius: var(--radius-full);
font-weight: 600;
min-width: 16px;
text-align: center;
}
.console-badge.error {
background: var(--danger-dim);
color: var(--danger);
}
.console-badge.warning {
background: var(--warning-dim);
color: var(--warning);
}
.console-toggle-icon {
font-size: var(--font-size-xs);
color: var(--muted);
transition: transform 0.3s ease;
}
.console-drawer.open .console-toggle-icon {
transform: rotate(180deg);
}
/* === Console Toolbar === */
.console-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-1) var(--space-3);
background: rgba(0, 0, 0, 0.3);
border-bottom: 1px solid var(--border);
}
.console-filters {
display: flex;
gap: 2px;
overflow-x: auto;
scrollbar-width: none;
}
.console-filters::-webkit-scrollbar { display: none; }
.console-filter {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: inherit;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--muted);
cursor: pointer;
transition: all var(--transition);
white-space: nowrap;
}
.console-filter:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--fg);
}
.console-filter.active {
background: var(--accent-dim);
color: var(--accent);
border-color: rgba(72, 187, 120, 0.3);
}
.filter-icon {
font-size: var(--font-size-xs);
font-weight: bold;
font-family: monospace;
}
.filter-icon.error { color: var(--danger); }
.filter-icon.warning { color: var(--warning); }
.filter-icon.success { color: var(--accent); }
.filter-count {
font-size: 9px;
opacity: 0.7;
font-weight: 600;
}
.console-actions {
display: flex;
gap: 2px;
}
.console-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0.5;
transition: all var(--transition);
font-size: var(--font-size-xs);
color: var(--fg);
}
.console-btn:hover {
background: rgba(255, 255, 255, 0.1);
opacity: 1;
}
/* === Console Body === */
.console-body {
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-2) var(--space-3);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: var(--font-size-xs);
line-height: 1.5;
width: 100%;
box-sizing: border-box;
writing-mode: horizontal-tb;
text-orientation: mixed;
}
/* === Console Line === */
.console-line {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
border-left: 2px solid transparent;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
margin: 1px 0;
animation: fadeIn 0.2s ease-out;
transition: background var(--transition);
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.console-line:hover {
background: rgba(255, 255, 255, 0.02);
}
.console-line.hidden { display: none; }
.console-line.error {
border-left-color: var(--danger);
background: rgba(245, 101, 101, 0.05);
color: var(--danger);
}
.console-line.success {
border-left-color: var(--accent);
color: var(--accent);
}
.console-line.warning {
border-left-color: var(--warning);
background: rgba(236, 201, 75, 0.03);
color: var(--warning);
}
.console-line.info {
color: var(--muted);
}
.console-icon {
flex-shrink: 0;
flex-grow: 0;
width: var(--space-3);
text-align: center;
font-size: var(--font-size-xs);
font-weight: bold;
font-family: monospace;
opacity: 0.8;
}
.console-time {
flex-shrink: 0;
flex-grow: 0;
font-size: 9px;
color: var(--muted);
opacity: 0.5;
font-family: monospace;
min-width: 45px;
}
.console-msg {
flex: 1 1 0;
min-width: 0;
word-break: break-word;
overflow-wrap: break-word;
white-space: normal;
writing-mode: horizontal-tb;
text-orientation: mixed;
display: inline;
}
.console-msg span {
word-break: break-word;
white-space: normal;
writing-mode: horizontal-tb;
display: inline;
}
.console-count {
flex-shrink: 0;
flex-grow: 0;
font-size: 9px;
font-weight: 600;
padding: 1px 5px;
background: var(--bg-elevated);
color: var(--muted);
border-radius: var(--radius-full);
margin-left: var(--space-1);
}
/* === Narrow screens - force horizontal text === */
@media (max-width: 400px) {
.console-body {
overflow-x: hidden;
width: 100%;
}
.console-line {
flex-direction: row !important;
flex-wrap: nowrap !important;
}
.console-time {
display: none;
}
.console-count {
display: none;
}
.console-msg {
white-space: normal !important;
writing-mode: horizontal-tb !important;
word-break: break-all !important;
}
}
@media (max-width: 250px) {
.console-icon {
width: 10px;
font-size: 8px;
}
.console-msg {
font-size: 8px;
}
}
/* === Syntax Highlighting === */
.console-msg .hl-path {
color: #6a9fb5;
word-break: break-all;
display: inline;
}
.console-msg .hl-url {
color: #6a9fb5;
text-decoration: underline dotted;
word-break: break-all;
display: inline;
}
.console-msg .hl-number {
color: #d19a66;
display: inline;
}
.console-msg .hl-string {
color: #98c379;
word-break: break-word;
display: inline;
}
.console-msg .hl-keyword {
color: #c678dd;
font-weight: 600;
display: inline;
}
.console-msg .hl-email {
color: #61afef;
word-break: break-all;
display: inline;
}
/* === Force horizontal text in all console elements === */
.console-drawer,
.console-drawer *,
.console-body,
.console-body *,
.console-line,
.console-line *,
.console-msg,
.console-msg * {
writing-mode: horizontal-tb !important;
text-orientation: mixed !important;
}
/* === Tab Bar === */
.tab-bar {
display: flex;
gap: var(--space-1);
padding: var(--space-1) var(--padding-main);
background: var(--bg-elevated);
border-bottom: 1px solid var(--border);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
flex-shrink: 0;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--muted);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background-color var(--transition),
color var(--transition),
box-shadow var(--transition),
transform var(--transition-fast);
white-space: nowrap;
min-width: 0;
position: relative;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.tab-item:focus { outline: none; }
.tab-item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.tab-item:hover {
background: var(--bg-hover);
color: var(--fg);
}
.tab-item.active {
background: var(--accent-dim);
color: var(--accent);
font-weight: 600;
}
/* Active tab glow indicator - more prominent */
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 3px;
background: var(--accent);
border-radius: 3px 3px 0 0;
box-shadow: 0 0 12px var(--accent-glow), 0 0 4px var(--accent);
}
.tab-icon {
font-size: var(--icon-sm);
flex-shrink: 0;
}
.tab-icon svg {
width: var(--icon-sm);
height: var(--icon-sm);
}
.tab-label {
overflow: hidden;
text-overflow: ellipsis;
}
.tab-badge {
font-size: 9px;
padding: 1px 5px;
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.1);
color: var(--muted);
flex-shrink: 0;
}
.tab-item.active .tab-badge {
background: var(--accent);
color: #fff;
}
/* === Tab Content === */
.tab-content {
display: none;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
animation: fadeIn 0.2s ease-out;
}
.tab-content.active {
display: flex;
flex-direction: column;
}
/* === Narrow: icons only === */
@media (max-width: 250px) {
.tab-bar {
padding: var(--space-1);
gap: 2px;
}
.tab-item {
padding: var(--space-2);
min-width: 32px;
}
.tab-label { display: none; }
.tab-badge { display: none; }
.tab-icon { font-size: var(--icon-md); }
}
/* === Medium: compact labels === */
@media (min-width: 251px) and (max-width: 400px) {
.tab-label { display: none; }
.tab-item { padding: var(--space-2); }
}
/* === Wide: full labels === */
@media (min-width: 401px) {
.tab-item {
padding: var(--space-2) var(--space-4);
}
}
/* === Toast Container === */
.toast-container {
position: fixed;
bottom: 48px;
left: 50%;
transform: translateX(-50%);
z-index: var(--z-toast);
display: flex;
flex-direction: column;
gap: var(--space-2);
pointer-events: none;
max-width: calc(100% - var(--space-4));
}
/* === Toast Message === */
.toast {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
font-size: var(--font-size-sm);
pointer-events: auto;
animation: toastSlideIn 0.3s ease;
max-width: 320px;
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.toast.toast-exit {
animation: toastSlideOut 0.2s ease forwards;
}
@keyframes toastSlideOut {
to {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
}
.toast-icon {
font-size: var(--font-size-md);
flex-shrink: 0;
}
.toast-message {
flex: 1;
min-width: 0;
word-break: break-word;
}
.toast-close {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition);
flex-shrink: 0;
}
.toast-close:hover {
background: var(--bg-hover);
color: var(--fg);
}
/* Toast Types */
.toast.success {
border-color: var(--accent);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, var(--bg) 100%);
}
.toast.success .toast-icon {
color: var(--accent);
}
.toast.error {
border-color: var(--danger);
background: linear-gradient(135deg, rgba(245, 101, 101, 0.1) 0%, var(--bg) 100%);
}
.toast.error .toast-icon {
color: var(--danger);
}
.toast.warning {
border-color: var(--warning);
background: linear-gradient(135deg, rgba(236, 201, 75, 0.1) 0%, var(--bg) 100%);
}
.toast.warning .toast-icon {
color: var(--warning);
}
.toast.info {
border-color: #63b3ed;
background: linear-gradient(135deg, rgba(99, 179, 237, 0.1) 0%, var(--bg) 100%);
}
.toast.info .toast-icon {
color: #63b3ed;
}
/* === Narrow: full-width toasts === */
@media (max-width: 250px) {
.toast-container {
left: var(--space-2);
right: var(--space-2);
transform: none;
max-width: none;
}
.toast {
max-width: none;
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
}
/* === Skeleton Base === */
.skeleton {
pointer-events: none;
}
.skeleton-pulse {
background: linear-gradient(
90deg,
var(--bg-elevated) 25%,
rgba(255, 255, 255, 0.08) 50%,
var(--bg-elevated) 75%
);
background-size: 200% 100%;
animation: skeletonPulse 1.5s ease-in-out infinite;
}
@keyframes skeletonPulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* === Skeleton Variants === */
.skeleton-line {
height: var(--space-3);
border-radius: var(--radius-sm);
margin: var(--space-1) 0;
background: var(--bg-elevated);
}
.skeleton-line.skeleton-pulse {
background: linear-gradient(
90deg,
var(--bg-elevated) 25%,
rgba(255, 255, 255, 0.08) 50%,
var(--bg-elevated) 75%
);
background-size: 200% 100%;
}
.skeleton-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-elevated);
}
.skeleton-text {
height: 12px;
border-radius: var(--radius-sm);
background: var(--bg-elevated);
}
.skeleton-text.short { width: 40%; }
.skeleton-text.medium { width: 60%; }
.skeleton-text.long { width: 80%; }
.skeleton-text.full { width: 100%; }
.skeleton-card {
padding: var(--space-3);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-2);
}
/* === Account Skeleton === */
.account.skeleton {
opacity: 0.6;
}
.account.skeleton .account-avatar {
background: var(--bg-elevated);
animation: skeletonPulse 1.5s ease-in-out infinite;
background-size: 200% 100%;
background-image: linear-gradient(
90deg,
var(--bg-elevated) 25%,
rgba(255, 255, 255, 0.08) 50%,
var(--bg-elevated) 75%
);
}
.account.skeleton .account-email,
.account.skeleton .account-meta {
background: var(--bg-elevated);
color: transparent;
border-radius: var(--radius-sm);
animation: skeletonPulse 1.5s ease-in-out infinite;
background-size: 200% 100%;
background-image: linear-gradient(
90deg,
var(--bg-elevated) 25%,
rgba(255, 255, 255, 0.08) 50%,
var(--bg-elevated) 75%
);
}
/* === Shimmer Effect === */
.skeleton-shimmer {
position: relative;
overflow: hidden;
background: var(--bg-elevated);
}
.skeleton-shimmer::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.06),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* === Skeleton Fade Out === */
.skeleton-fade-out {
animation: skeletonFadeOut 0.2s ease forwards;
}
@keyframes skeletonFadeOut {
to {
opacity: 0;
transform: scale(0.95);
}
}
/* === AutoReg Controls === */
.autoreg-controls {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
margin: 0 var(--padding-main) var(--space-2);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
flex-wrap: wrap;
}
/* Running state */
.autoreg-controls.running {
border-color: var(--accent);
background: linear-gradient(135deg, rgba(72, 187, 120, 0.08) 0%, transparent 100%);
}
.autoreg-controls.running .btn {
flex: 1;
min-width: 60px;
}
/* Count input */
.autoreg-count {
width: 50px;
height: 32px;
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-sm);
font-family: inherit;
font-weight: 600;
text-align: center;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
transition: all var(--transition);
flex-shrink: 0;
}
.autoreg-count:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.autoreg-count::-webkit-inner-spin-button,
.autoreg-count::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Start button */
.btn-start {
height: 32px;
padding: 0 var(--space-3);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-size: var(--font-size-sm);
white-space: nowrap;
flex-shrink: 0;
}
/* === Strategy Switch === */
.strategy-switch {
display: flex;
flex: 1;
min-width: 100px;
background: var(--bg-elevated);
border-radius: var(--radius-md);
padding: 2px;
gap: 2px;
}
.strategy-sw-btn {
flex: 1;
padding: var(--space-2) var(--space-3);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: inherit;
background: transparent;
color: var(--muted);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition);
white-space: nowrap;
min-width: 0;
}
.strategy-sw-btn .sw-icon {
font-size: var(--font-size-sm);
flex-shrink: 0;
}
.strategy-sw-btn .sw-label {
overflow: hidden;
text-overflow: ellipsis;
}
.strategy-sw-btn:hover:not(.active) {
color: var(--fg);
}
.strategy-sw-btn.active {
background: var(--accent);
color: #fff;
box-shadow: var(--shadow-sm);
}
/* === Narrow: stack vertically === */
@media (max-width: 250px) {
.autoreg-controls {
padding: var(--space-1);
margin: 0 var(--space-1) var(--space-1);
gap: var(--space-1);
}
.strategy-switch {
flex: 1 1 100%;
order: 1;
}
.autoreg-count {
width: 40px;
height: 28px;
font-size: var(--font-size-xs);
order: 2;
}
.btn-start {
flex: 1;
height: 28px;
order: 3;
}
.btn-start .btn-text { display: none; }
.strategy-sw-btn .sw-label { display: none; }
.strategy-sw-btn { padding: var(--space-2); }
}
/* === Medium: compact === */
@media (min-width: 251px) and (max-width: 350px) {
.strategy-sw-btn .sw-label { display: none; }
}
/* === Settings Cards (Compact) === */
.settings-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-3);
overflow: hidden;
}
.settings-card-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: rgba(0, 0, 0, 0.15);
border-bottom: 1px solid var(--border);
}
.settings-card-icon {
font-size: var(--font-size-lg);
}
.settings-card-title {
flex: 1;
font-size: var(--font-size-sm);
font-weight: 600;
}
.settings-card-body {
padding: var(--space-1) var(--space-3);
}
.settings-card .setting-row {
padding: var(--space-3) 0;
}
.settings-card .setting-row:last-child {
border-bottom: none;
}
/* Active Profile in Card */
.settings-card .active-profile-content {
padding: var(--space-3);
}
.settings-card .active-profile-empty {
display: flex;
align-items: center;
gap: var(--space-3);
}
.settings-card .active-profile-empty .empty-text {
flex: 1;
font-size: var(--font-size-xs);
color: var(--muted);
}
.settings-card .active-profile-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.settings-card .active-profile-avatar {
font-size: var(--font-size-xl);
}
.settings-card .active-profile-details {
flex: 1;
min-width: 0;
}
.settings-card .active-profile-name {
font-size: var(--font-size-sm);
font-weight: 600;
}
.settings-card .active-profile-email {
font-size: var(--font-size-xs);
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.settings-card .active-profile-strategy {
font-size: var(--font-size-xs);
color: var(--muted);
margin-top: 2px;
}
.settings-card .active-profile-stats {
display: flex;
gap: var(--space-3);
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.settings-card .active-profile-stat {
display: flex;
flex-direction: column;
align-items: center;
}
.settings-card .active-profile-stat-value {
font-size: var(--font-size-md);
font-weight: 700;
}
.settings-card .active-profile-stat-value.success { color: var(--accent); }
.settings-card .active-profile-stat-value.danger { color: var(--danger); }
.settings-card .active-profile-stat-label {
font-size: 9px;
color: var(--muted);
text-transform: uppercase;
}
/* Settings Card Footer */
.settings-card-footer {
padding: var(--space-3);
background: rgba(0, 0, 0, 0.1);
border-top: 1px solid var(--border);
}
/* === Form Groups === */
.form-group {
margin-bottom: var(--space-3);
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--fg);
margin-bottom: var(--space-2);
}
.form-control {
width: 100%;
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-sm);
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
transition: border-color var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--accent);
}
.form-control::placeholder {
color: var(--muted);
}
.form-group .setting-desc {
margin-top: var(--space-1);
}
/* === Spoof Section === */
.spoof-section {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-3);
overflow: hidden;
}
.spoof-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3);
background: rgba(0, 0, 0, 0.15);
}
.spoof-title {
display: flex;
align-items: center;
gap: var(--space-3);
}
.spoof-icon {
font-size: var(--font-size-lg);
}
.spoof-details {
padding: var(--space-3);
border-top: 1px solid var(--border);
}
.spoof-details.hidden {
display: none;
}
.spoof-modules {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
}
.spoof-module {
display: flex;
align-items: flex-start;
gap: var(--space-2);
padding: var(--space-2);
background: rgba(0, 0, 0, 0.1);
border-radius: var(--radius-md);
}
.module-icon {
font-size: var(--font-size-md);
}
.module-name {
font-size: var(--font-size-xs);
font-weight: 600;
}
.module-desc {
font-size: 9px;
color: var(--muted);
margin-top: 2px;
}
.spoof-warning {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-3);
padding: var(--space-2);
background: rgba(236, 201, 75, 0.1);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
color: var(--warning);
}
/* === Narrow: single column spoof modules === */
@media (max-width: 300px) {
.spoof-modules {
grid-template-columns: 1fr;
}
}
/* === LLM Status Badge (in header) === */
.llm-status-badge {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
margin-right: var(--space-2);
}
.llm-status-badge .patch-status {
font-size: 10px;
padding: 2px 8px;
}
/* === LLM Server Controls === */
.llm-controls {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.llm-status {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
background: var(--bg-elevated);
border-radius: var(--radius-md);
}
.llm-status-indicator {
display: flex;
align-items: center;
gap: var(--space-2);
}
.llm-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.llm-status-dot.running {
background: var(--accent);
box-shadow: 0 0 8px var(--accent-glow);
animation: pulse 1.5s ease-in-out infinite;
}
.llm-status-dot.stopped {
background: var(--danger);
}
.llm-status-text {
font-size: var(--font-size-sm);
font-weight: 500;
}
/* LLM Button Group - equal width buttons */
.llm-buttons {
display: flex;
gap: var(--space-2);
}
.llm-buttons .btn {
flex: 1;
min-width: 0;
max-width: none;
}
/* Stack vertically on narrow */
@media (max-width: 280px) {
.llm-buttons {
flex-direction: column;
}
.llm-buttons .btn {
width: 100%;
}
}
/* === Stats Dashboard === */
.stats-dashboard {
padding: var(--padding-section);
}
.stats-header {
margin-bottom: var(--space-4);
}
.stats-title {
font-size: var(--font-size-md);
font-weight: 600;
margin: 0;
}
/* === Stats Cards Grid === */
.stats-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
margin-bottom: var(--space-4);
}
.stat-card {
padding: var(--space-3);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
text-align: center;
transition: all var(--transition);
}
.stat-card:hover {
border-color: var(--border-strong);
}
.stat-card.success { border-color: rgba(72, 187, 120, 0.3); }
.stat-card.danger { border-color: rgba(245, 101, 101, 0.3); }
.stat-card.warning { border-color: rgba(236, 201, 75, 0.3); }
.stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
line-height: 1;
}
.stat-card.success .stat-value { color: var(--accent); }
.stat-card.danger .stat-value { color: var(--danger); }
.stat-card.warning .stat-value { color: var(--warning); }
.stat-label {
font-size: var(--font-size-xs);
color: var(--muted);
text-transform: uppercase;
margin-top: var(--space-1);
}
/* === Stats Sections === */
.stats-section {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: var(--space-3);
margin-bottom: var(--space-3);
}
.stats-section-title {
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
margin-bottom: var(--space-3);
}
/* === Usage Overview === */
.usage-overview {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
.usage-bar-container {
flex: 1;
min-width: 100px;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: var(--radius-sm);
overflow: hidden;
}
.usage-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
border-radius: var(--radius-sm);
transition: width 0.3s ease;
}
.usage-numbers {
font-size: var(--font-size-sm);
font-weight: 600;
white-space: nowrap;
}
.usage-current { color: var(--accent); }
.usage-separator { color: var(--muted); margin: 0 2px; }
.usage-limit { color: var(--muted); }
.usage-avg {
font-size: var(--font-size-xs);
color: var(--muted);
margin-top: var(--space-2);
width: 100%;
}
.usage-avg strong { color: var(--fg); }
/* === Mini Chart === */
.mini-chart {
display: flex;
align-items: flex-end;
gap: var(--space-1);
height: 60px;
padding: var(--space-1) 0;
}
.chart-bar {
flex: 1;
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-dim) 100%);
border-radius: 2px 2px 0 0;
min-height: var(--space-1);
transition: height 0.3s ease;
}
.chart-bar:hover {
background: var(--accent);
}
.chart-labels {
display: flex;
justify-content: space-between;
font-size: 9px;
color: var(--muted);
margin-top: var(--space-1);
}
/* === Health Bars === */
.health-bars {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.health-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.health-label {
font-size: var(--font-size-xs);
width: 60px;
white-space: nowrap;
flex-shrink: 0;
}
.health-bar {
flex: 1;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
}
.health-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.health-fill.success { background: var(--accent); }
.health-fill.danger { background: var(--danger); }
.health-fill.warning { background: var(--warning); }
.health-percent {
font-size: var(--font-size-xs);
font-weight: 600;
width: 32px;
text-align: right;
flex-shrink: 0;
}
/* === Wide: 4-column grid === */
@media (min-width: 500px) {
.stats-cards {
grid-template-columns: repeat(4, 1fr);
}
}
/* === Narrow: single column === */
@media (max-width: 250px) {
.stats-cards {
grid-template-columns: 1fr;
}
.stat-card {
padding: var(--space-2);
}
.stat-value {
font-size: var(--font-size-lg);
}
}
/* === Strategy Selection === */
.strategy-option {
display: flex;
gap: var(--space-3);
padding: var(--space-4);
border: 2px solid var(--glass-border);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-normal);
margin-bottom: var(--space-3);
position: relative;
}
.strategy-option:hover {
border-color: var(--accent);
background: var(--glass-bg);
}
.strategy-option.selected {
border-color: var(--accent);
background: rgba(72, 187, 120, 0.05);
}
.strategy-option.strategy-safe.selected {
border-color: #48bb78;
background: rgba(72, 187, 120, 0.08);
}
.strategy-option.strategy-risky.selected {
border-color: #ed8936;
background: rgba(237, 137, 54, 0.08);
}
.strategy-option input[type="radio"] {
margin-top: 2px;
cursor: pointer;
accent-color: var(--accent);
}
/* Strategy Icon */
.strategy-icon-wrapper {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.strategy-icon-safe {
background: rgba(72, 187, 120, 0.15);
border: 1px solid rgba(72, 187, 120, 0.3);
}
.strategy-icon-risky {
background: rgba(237, 137, 54, 0.15);
border: 1px solid rgba(237, 137, 54, 0.3);
}
.strategy-icon {
font-size: var(--font-size-xl);
}
.strategy-content {
flex: 1;
min-width: 0;
}
.strategy-header {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
flex-wrap: wrap;
}
.strategy-desc {
font-size: var(--font-size-sm);
color: var(--muted);
margin-bottom: var(--space-3);
line-height: 1.4;
}
.strategy-meta {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
/* === Strategy Features === */
.strategy-features {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
margin-bottom: var(--space-3);
}
.strategy-feature {
display: flex;
align-items: center;
gap: var(--space-1);
font-size: var(--font-size-xs);
}
.strategy-feature .feature-icon {
width: var(--space-3);
height: var(--space-3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: bold;
}
.strategy-feature.feature-pro .feature-icon {
background: rgba(72, 187, 120, 0.2);
color: #48bb78;
}
.strategy-feature.feature-pro {
color: #48bb78;
}
.strategy-feature.feature-con .feature-icon {
background: rgba(237, 137, 54, 0.2);
color: #ed8936;
}
.strategy-feature.feature-con {
color: #ed8936;
}
/* === Risk Bar === */
.strategy-risk-bar {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--glass-border);
}
.risk-label {
font-size: var(--font-size-xs);
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.risk-meter {
flex: 1;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
overflow: hidden;
}
.risk-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.risk-fill.risk-low {
background: linear-gradient(90deg, #48bb78, #68d391);
}
.risk-fill.risk-medium {
background: linear-gradient(90deg, #ed8936, #f6ad55);
}
.risk-fill.risk-high {
background: linear-gradient(90deg, #ed8936, #f56565);
}
.risk-value {
font-size: var(--font-size-xs);
font-weight: 600;
min-width: 45px;
text-align: right;
}
.risk-low-text { color: #48bb78; }
.risk-medium-text { color: #ed8936; }
.risk-high-text { color: #f56565; }
/* === Badge === */
.badge {
display: inline-block;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
white-space: nowrap;
}
.badge-success {
background: rgba(72, 187, 120, 0.2);
color: #48bb78;
}
.badge-warning {
background: rgba(237, 137, 54, 0.2);
color: #ed8936;
}
.badge-danger {
background: rgba(245, 101, 101, 0.2);
color: #f56565;
}
.badge-info {
background: rgba(99, 179, 237, 0.2);
color: #63b3ed;
}
/* === Narrow: compact strategy cards === */
@media (max-width: 250px) {
.strategy-option {
flex-direction: column;
padding: var(--space-3);
}
.strategy-icon-wrapper {
width: 36px;
height: 36px;
}
.strategy-icon {
font-size: var(--font-size-md);
}
.strategy-features {
display: none;
}
}
/* === Batch Reg Card === */
.batch-reg-card {
margin: var(--space-2) var(--padding-main);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all var(--transition);
}
.batch-reg-card:hover {
border-color: var(--border-strong);
}
.batch-reg-card.running {
border-color: var(--accent);
box-shadow: var(--shadow-glow);
}
.batch-reg-card.collapsed .batch-reg-body {
display: none;
}
.batch-reg-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
background: rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: background var(--transition);
}
.batch-reg-header:hover {
background: rgba(0, 0, 0, 0.15);
}
.batch-reg-title {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--font-size-sm);
font-weight: 600;
}
.batch-reg-icon {
font-size: var(--font-size-md);
}
.batch-reg-badge {
font-size: 9px;
padding: 2px var(--space-2);
border-radius: var(--radius-full);
font-weight: 600;
}
.batch-reg-badge.running {
background: var(--accent-dim);
color: var(--accent);
animation: pulse 1.5s ease-in-out infinite;
}
.batch-reg-badge.complete {
background: rgba(72, 187, 120, 0.2);
color: var(--accent);
}
.batch-reg-toggle {
font-size: var(--font-size-xs);
color: var(--muted);
transition: transform 0.2s ease;
}
.batch-reg-card.collapsed .batch-reg-toggle {
transform: rotate(-90deg);
}
.batch-reg-body {
padding: var(--space-4);
}
/* === Progress Status === */
.batch-reg-status {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
background: rgba(0, 0, 0, 0.15);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.batch-reg-progress-ring {
position: relative;
width: 64px;
height: 64px;
flex-shrink: 0;
}
.batch-reg-progress-ring svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.batch-reg-progress-ring .progress-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 3;
}
.batch-reg-progress-ring .progress-fill {
fill: none;
stroke: var(--accent);
stroke-width: 3;
stroke-linecap: round;
transition: stroke-dasharray 0.5s ease;
}
.batch-reg-progress-ring .progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: var(--font-size-sm);
font-weight: 700;
color: var(--fg);
}
.batch-reg-status-info {
flex: 1;
min-width: 120px;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.status-line {
display: flex;
justify-content: space-between;
font-size: var(--font-size-sm);
}
.status-label {
color: var(--muted);
}
.status-value {
font-weight: 600;
color: var(--fg);
}
.status-line.highlight .status-value {
color: var(--accent);
font-size: var(--font-size-md);
font-weight: 800;
}
/* === Settings Form === */
.batch-reg-settings {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.batch-reg-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.batch-reg-field > label {
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Count selector */
.batch-reg-count-group {
display: flex;
align-items: center;
background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.count-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--fg);
font-size: var(--font-size-lg);
font-weight: 300;
cursor: pointer;
transition: all var(--transition);
}
.count-btn:hover:not(:disabled) {
background: var(--accent-dim);
color: var(--accent);
}
.count-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.count-input {
flex: 1;
height: 40px;
border: none;
background: transparent;
color: var(--fg);
font-size: var(--font-size-lg);
font-weight: 600;
text-align: center;
font-family: inherit;
}
.count-input:focus {
outline: none;
}
/* Interval pills */
.batch-reg-interval-pills {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.interval-pill {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--input-border);
border-radius: var(--radius-full);
background: var(--input-bg);
color: var(--muted);
font-size: var(--font-size-xs);
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: all var(--transition);
flex: 1;
min-width: 60px;
text-align: center;
}
.interval-pill:hover {
border-color: var(--accent);
color: var(--fg);
}
.interval-pill.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.batch-reg-hint {
font-size: var(--font-size-xs);
color: var(--muted);
}
/* Name mode */
.batch-reg-name-mode {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.name-mode-option {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3);
border: 1px solid var(--input-border);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition);
}
.name-mode-option:hover {
border-color: rgba(72, 187, 120, 0.4);
}
.name-mode-option.active {
border-color: var(--accent);
background: var(--accent-dim);
}
.name-mode-option input[type="radio"] {
margin-top: 2px;
accent-color: var(--accent);
}
.name-mode-label {
display: flex;
flex-direction: column;
gap: 2px;
}
.name-mode-title {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--fg);
}
.name-mode-desc {
font-size: var(--font-size-xs);
color: var(--muted);
}
.batch-reg-input {
width: 100%;
padding: var(--space-3);
font-size: var(--font-size-sm);
font-family: inherit;
background: var(--input-bg);
color: var(--fg);
border: 1px solid var(--input-border);
border-radius: var(--radius-md);
margin-top: var(--space-2);
transition: all var(--transition);
}
.batch-reg-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
/* Preview */
.batch-reg-preview {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: rgba(72, 187, 120, 0.06);
border-radius: var(--radius-md);
flex-wrap: wrap;
}
.preview-label {
font-size: var(--font-size-xs);
color: var(--muted);
font-weight: 600;
}
.preview-names {
display: flex;
gap: var(--space-1);
flex-wrap: wrap;
}
.preview-name {
font-size: var(--font-size-xs);
color: var(--accent);
padding: 2px var(--space-2);
background: var(--bg);
border-radius: var(--radius-sm);
}
/* === Narrow: compact layout === */
@media (max-width: 250px) {
.batch-reg-card {
margin: var(--space-1);
}
.batch-reg-header {
padding: var(--space-2);
}
.batch-reg-body {
padding: var(--space-2);
}
.batch-reg-status {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
.batch-reg-progress-ring {
width: 48px;
height: 48px;
align-self: center;
}
}
/* === Ultra-Narrow (< 150px): Minimal survival mode === */
@media (max-width: 150px) {
:root {
--padding-main: 2px;
--padding-card: 4px;
}
/* Extreme compression */
.header { padding: 2px; gap: 2px; }
.header-title { display: none; }
.header-badge { display: none; }
.tab-bar { padding: 2px; }
.tab-item { padding: 4px; min-width: 24px; }
.tab-label, .tab-badge { display: none !important; }
.hero { margin: 2px; padding: 4px; }
.hero-value { font-size: 16px; }
.hero-label { font-size: 8px; }
.hero-footer { display: none; }
.hero-progress { height: 4px; }
.toolbar { padding: 2px; }
.search-wrapper { display: none; }
.filter-group { display: none; }
.account { padding: 4px; gap: 4px; }
.account-avatar { width: 20px; height: 20px; font-size: 8px; }
.account-email { font-size: 9px; }
.account-meta { display: none; }
.account-actions { gap: 2px; }
.account-btn { width: 24px; height: 24px; min-width: 24px; }
.console-drawer { transform: translateY(100%); }
}
/* === Narrow (150px - 250px): Stream Deck mode === */
@media (min-width: 151px) and (max-width: 250px) {
:root {
--padding-main: 4px;
--padding-card: 6px;
--padding-section: 8px;
}
/* Hide all text labels */
.btn .btn-text,
.tab-label,
.filter-label,
.header-badge,
.strategy-sw-btn .sw-label {
display: none !important;
}
/* Stack LLM buttons vertically */
.btn-group {
flex-direction: column;
}
.btn-group .btn {
width: 100%;
max-width: none;
}
/* Compact everything */
.header { padding: var(--space-1) var(--space-2); }
.header-title { font-size: 11px; max-width: 80px; }
.toolbar { padding: var(--space-1); }
.toolbar-row { flex-direction: column; gap: var(--space-1); }
.hero { margin: var(--space-1); padding: var(--space-2); }
.hero-footer { display: none; }
.account { padding: var(--space-2); }
.account-meta { font-size: 9px; }
body { font-size: 11px; }
}
/* === Medium (250px - 500px): Standard sidebar === */
@media (min-width: 251px) and (max-width: 500px) {
:root {
--padding-main: 8px;
--padding-card: 10px;
--padding-section: 12px;
}
/* Show labels on wider medium screens */
@media (min-width: 350px) {
.tab-label { display: inline; }
.btn .btn-text { display: inline; }
}
/* Hero footer visible */
.hero-footer { display: grid; }
}
/* === Wide (500px - 750px): Dashboard mode === */
@media (min-width: 501px) and (max-width: 750px) {
:root {
--padding-main: 12px;
--padding-card: 14px;
--padding-section: 16px;
}
/* 2-column grid for accounts */
.list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.list-group {
grid-column: 1 / -1;
}
.account {
margin-bottom: 0;
}
/* Settings in 2 columns */
.settings-card-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.setting-row.full-width {
grid-column: 1 / -1;
}
/* Constrain button widths */
.btn:not(.btn-block) {
max-width: 180px;
}
}
/* === Extra Wide (750px+): Full dashboard === */
@media (min-width: 751px) {
:root {
--padding-main: 16px;
--padding-card: 16px;
--padding-section: 20px;
}
/* 3-column grid for accounts */
.list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-4);
}
/* Stats in 4 columns */
.stats-cards {
grid-template-columns: repeat(4, 1fr);
}
/* Batch reg settings in 2 columns */
.batch-reg-settings {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-4);
}
.batch-reg-field.full-width {
grid-column: 1 / -1;
}
/* Wider modals */
.modal {
max-width: 520px;
}
/* Constrain button widths more */
.btn:not(.btn-block) {
max-width: 160px;
}
}
/* === Touch Device Adjustments === */
@media (hover: none) {
/* Always show action buttons */
.account-actions,
.profile-actions,
.profile-card-actions {
opacity: 1 !important;
}
/* Larger touch targets */
.btn {
min-height: 40px;
}
.icon-btn {
width: 40px;
height: 40px;
min-width: 40px;
}
.account-btn {
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
}
/* Disable hover transforms */
.btn:hover,
.account:hover,
.settings-card-header:hover {
transform: none;
}
}
/* === High Contrast Mode === */
@media (prefers-contrast: high) {
:root {
--border: rgba(255, 255, 255, 0.25);
--border-strong: rgba(255, 255, 255, 0.5);
--muted: #b0b0b0;
}
.btn {
border: 1px solid currentColor;
}
.account {
border-width: 2px;
}
}
/* === Reduced Motion === */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* === Print Styles === */
@media print {
.console-drawer,
.toast-container,
.modal-overlay,
.dialog-overlay {
display: none !important;
}
body {
background: white;
color: black;
}
}
/* === Profile Section === */
.profiles-section {
padding: 12px;
}
.profiles-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.profiles-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.profiles-empty {
text-align: center;
padding: 30px 20px;
color: var(--muted);
}
.profiles-empty .empty-icon {
font-size: 36px;
margin-bottom: 10px;
opacity: 0.5;
}
.profiles-empty .empty-text {
margin-bottom: 16px;
font-size: 12px;
}
.profiles-add {
margin-top: 10px;
width: 100%;
}
/* === Profile Card === */
.imap-profile {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.imap-profile:hover {
border-color: rgba(63,182,139,0.3);
background: linear-gradient(135deg, rgba(63,182,139,0.06) 0%, transparent 100%);
transform: translateY(-1px);
}
.imap-profile.active {
border-color: var(--accent);
background: linear-gradient(135deg, var(--accent-dim) 0%, transparent 100%);
}
.imap-profile.exhausted {
opacity: 0.6;
border-color: var(--danger);
}
.imap-profile.error {
border-color: var(--danger);
}
.imap-profile.paused {
opacity: 0.7;
border-color: var(--warning);
}
.profile-header {
display: flex;
align-items: center;
gap: 10px;
}
.profile-avatar {
position: relative;
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.profile-strategy-icon {
font-size: 16px;
}
.profile-status-dot {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--bg);
}
.profile-info {
flex: 1;
min-width: 0;
}
.profile-name-row {
display: flex;
align-items: center;
gap: 6px;
}
.profile-name {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-provider {
font-size: 9px;
padding: 2px 6px;
border-radius: 8px;
background: var(--bg-elevated);
color: var(--muted);
font-weight: 500;
}
.profile-email {
font-size: 11px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity var(--transition);
}
.imap-profile:hover .profile-actions {
opacity: 1;
}
.profile-btn {
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--muted);
transition: all var(--transition);
}
.profile-btn:hover {
background: rgba(128,128,128,0.2);
color: var(--fg);
}
.profile-btn.danger:hover {
background: var(--danger-dim);
border-color: var(--danger);
color: var(--danger);
}
.profile-btn svg {
width: 14px;
height: 14px;
}
.profile-details {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.profile-strategy {
display: flex;
align-items: center;
gap: 8px;
}
.profile-pool-stats {
font-size: 10px;
padding: 2px 6px;
background: var(--accent-dim);
color: var(--accent);
border-radius: 8px;
}
.profile-stats {
display: flex;
gap: 12px;
}
.profile-stats .stat {
display: flex;
flex-direction: column;
align-items: center;
}
.profile-stats .stat-value {
font-size: 12px;
font-weight: 700;
font-family: var(--vscode-editor-font-family, monospace);
}
.profile-stats .stat-label {
font-size: 9px;
color: var(--muted);
}
.profile-error {
margin-top: 8px;
padding: 6px 8px;
background: var(--danger-dim);
border-radius: var(--radius-sm);
font-size: 10px;
color: var(--danger);
display: flex;
align-items: center;
gap: 6px;
}
/* === Profile Editor === */
.profile-editor {
position: fixed;
inset: 0;
background: var(--bg);
z-index: 200;
display: none;
flex-direction: column;
animation: slideIn 0.25s ease;
}
.profile-editor.visible,
.profile-editor[style*="display: flex"] {
display: flex;
}
.editor-header {
display: flex;
align-items: center;
gap: 12px;
height: 42px;
padding: 0 12px;
background: linear-gradient(180deg, var(--bg-elevated) 0%, transparent 100%);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.editor-title {
font-size: 13px;
font-weight: 600;
}
.editor-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.editor-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 8px;
background: var(--bg-elevated);
}
/* === Form Section === */
.form-section {
margin-bottom: 20px;
padding: 14px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
}
.form-section-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
}
.form-section-desc {
font-size: 11px;
color: var(--muted);
margin-bottom: 12px;
}
/* === Strategy Selector === */
.strategy-selector {
display: flex;
flex-direction: column;
gap: 8px;
}
.strategy-option {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-option:hover {
border-color: rgba(63,182,139,0.3);
background: rgba(63,182,139,0.05);
}
.strategy-option.selected {
border-color: var(--accent);
background: var(--accent-dim);
}
.strategy-icon {
font-size: 20px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--glass-bg);
border-radius: var(--radius-sm);
}
.strategy-content {
flex: 1;
}
.strategy-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 2px;
color: var(--accent);
}
.strategy-desc {
font-size: 11px;
color: var(--muted);
line-height: 1.4;
}
.strategy-example {
font-size: 10px;
color: var(--accent);
font-family: var(--vscode-editor-font-family, monospace);
margin-top: 4px;
padding: 3px 6px;
background: var(--accent-dim);
border-radius: var(--radius-sm);
display: inline-block;
}
.config-hint {
font-size: 11px;
color: var(--muted);
padding: 10px 12px;
background: var(--bg-elevated);
border-radius: var(--radius-sm);
margin-bottom: 12px;
line-height: 1.5;
}
.strategy-check {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
opacity: 0;
transition: opacity var(--transition);
}
.strategy-option.selected .strategy-check {
opacity: 1;
}
.strategy-check svg {
width: 16px;
height: 16px;
}
.strategy-config {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
/* === Email Pool Editor === */
.email-pool-editor {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.pool-list {
max-height: 200px;
overflow-y: auto;
}
.pool-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
font-size: 11px;
}
.pool-item:last-child {
border-bottom: none;
}
.pool-item.used {
opacity: 0.6;
background: rgba(63,182,139,0.1);
}
.pool-item.failed {
background: var(--danger-dim);
}
.pool-status {
font-size: 12px;
}
.pool-stats {
display: flex;
gap: 12px;
padding: 8px 10px;
background: var(--glass-bg);
border-bottom: 1px solid var(--border);
font-size: 11px;
}
.pool-stat {
color: var(--muted);
}
.pool-stat.success {
color: var(--success);
}
.pool-stat.danger {
color: var(--danger);
}
.pool-email {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pool-remove {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition);
}
.pool-remove:hover {
background: var(--danger-dim);
color: var(--danger);
}
.pool-remove:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.pool-add {
display: flex;
gap: 6px;
padding: 8px;
border-top: 1px solid var(--border);
}
.pool-add .form-input {
flex: 1;
}
.pool-add .btn {
padding: 8px 12px;
}
.pool-actions {
display: flex;
gap: 6px;
padding: 8px;
border-top: 1px solid var(--border);
background: var(--glass-bg);
}
.pool-actions .btn {
flex: 1;
font-size: 10px;
}
/* === Password Input === */
.password-input-wrapper {
position: relative;
}
.password-input-wrapper .form-input {
padding-right: 36px;
}
.password-toggle {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition);
}
.password-toggle:hover {
background: var(--bg-elevated);
color: var(--fg);
}
.password-toggle svg {
width: 14px;
height: 14px;
}
/* === Settings Section === */
.setting-section {
margin-bottom: 16px;
padding: 14px;
background: linear-gradient(135deg, rgba(63,182,139,0.08) 0%, transparent 100%);
border: 1px solid rgba(63,182,139,0.2);
border-radius: var(--radius-md);
}
.setting-section-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.setting-section-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
}
.setting-section-desc {
font-size: 11px;
color: var(--muted);
}
/* === Active Profile Card === */
.active-profile-card {
margin-bottom: 16px;
padding: 14px;
background: linear-gradient(135deg, rgba(63,182,139,0.1) 0%, rgba(63,182,139,0.03) 100%);
border: 1px solid rgba(63,182,139,0.25);
border-radius: var(--radius-lg);
}
.active-profile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.active-profile-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--accent);
}
.btn-sm {
padding: 5px 10px;
font-size: 10px;
}
.active-profile-content {
min-height: 60px;
}
.active-profile-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px;
text-align: center;
}
.active-profile-empty .empty-icon {
font-size: 24px;
opacity: 0.5;
}
.active-profile-empty .empty-text {
font-size: 11px;
color: var(--muted);
}
.active-profile-info {
display: flex;
align-items: center;
gap: 12px;
}
.active-profile-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.active-profile-details {
flex: 1;
min-width: 0;
}
.active-profile-name {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.active-profile-email {
font-size: 11px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 6px;
}
.active-profile-strategy {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 10px;
}
.active-profile-strategy .strategy-icon {
font-size: 12px;
}
.active-profile-strategy .strategy-name {
font-weight: 600;
color: var(--accent);
}
.active-profile-strategy .strategy-desc {
color: var(--muted);
margin-left: 4px;
}
.active-profile-stats {
display: flex;
gap: 16px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(63,182,139,0.15);
}
.active-profile-stat {
display: flex;
flex-direction: column;
align-items: center;
}
.active-profile-stat-value {
font-size: 14px;
font-weight: 700;
font-family: var(--vscode-editor-font-family, monospace);
}
.active-profile-stat-value.success { color: var(--accent); }
.active-profile-stat-value.danger { color: var(--danger); }
.active-profile-stat-label {
font-size: 9px;
color: var(--muted);
text-transform: uppercase;
}
/* === Profile Card (new style) === */
.profile-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 12px;
transition: all 0.2s ease;
}
.profile-card:hover {
border-color: rgba(63,182,139,0.3);
background: linear-gradient(135deg, rgba(63,182,139,0.06) 0%, transparent 100%);
}
.profile-card.active {
border-color: var(--accent);
background: linear-gradient(135deg, var(--accent-dim) 0%, transparent 100%);
}
.profile-card-header {
display: flex;
align-items: center;
gap: 10px;
}
.profile-card-radio {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.radio-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #666;
background: transparent;
transition: all 0.2s ease;
flex-shrink: 0;
}
.profile-card-radio:hover .radio-dot {
border-color: var(--accent);
}
.radio-dot.checked {
border-color: var(--accent);
background: var(--accent);
box-shadow: inset 0 0 0 3px var(--bg);
}
.profile-card-info {
flex: 1;
min-width: 0;
cursor: pointer;
}
.profile-card-name {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-card-email {
font-size: 11px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-card-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity var(--transition);
}
.profile-card:hover .profile-card-actions {
opacity: 1;
}
.profile-card-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
font-size: 10px;
color: var(--muted);
}
.profile-strategy {
display: flex;
align-items: center;
gap: 4px;
}
.profile-stats {
font-family: var(--vscode-editor-font-family, monospace);
}
.profiles-add-btn {
margin-top: 12px;
width: 100%;
}
/* === Form Elements === */
.form-group {
margin-bottom: 12px;
}
.form-label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--muted);
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 8px 10px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--fg);
font-size: 12px;
transition: all var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim);
}
.form-input::placeholder {
color: var(--muted);
opacity: 0.6;
}
.form-row {
display: flex;
gap: 10px;
}
.form-row .form-group {
flex: 1;
}
.form-row .form-group.flex-1 {
flex: 1;
}
.form-row .form-group.flex-2 {
flex: 2;
}
.form-hint {
font-size: 10px;
color: var(--muted);
margin-top: 4px;
display: none;
}
.form-hint .provider-name {
color: var(--accent);
font-weight: 600;
}
.form-hint .password-hint {
display: block;
margin-top: 6px;
padding: 6px 8px;
background: var(--bg-elevated);
border-radius: 4px;
border-left: 3px solid var(--accent);
font-size: 11px;
line-height: 1.4;
}
.form-hint .password-hint.warning {
border-left-color: #f0ad4e;
background: rgba(240, 173, 78, 0.1);
}
/* === Profiles Panel Overlay === */
.profiles-panel {
position: fixed;
inset: 0;
background: var(--bg);
z-index: 150;
display: none;
flex-direction: column;
animation: slideIn 0.25s ease;
}
.profiles-panel.visible {
display: flex;
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.profiles-panel-header {
display: flex;
align-items: center;
gap: 12px;
height: 42px;
padding: 0 12px;
background: linear-gradient(180deg, var(--bg-elevated) 0%, transparent 100%);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.profiles-panel-title {
font-size: 13px;
font-weight: 600;
flex: 1;
}
.profiles-panel-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
/* ========================================
WIZARD STYLES
======================================== */
.profile-editor.wizard {
display: flex;
}
.wizard-step {
margin-bottom: 20px;
padding: 16px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
animation: fadeIn 0.3s ease;
}
.wizard-step.compact {
padding: 12px 16px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.step-number {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
color: white;
font-size: 13px;
font-weight: 700;
border-radius: 50%;
}
.step-title {
font-size: 14px;
font-weight: 600;
}
/* Email Input Section */
.email-input-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.form-input-large {
padding: 14px 16px;
font-size: 15px;
border-radius: var(--radius-md);
}
/* Provider Detection */
.provider-detection {
animation: fadeIn 0.3s ease;
}
.provider-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: linear-gradient(135deg, rgba(63,182,139,0.15) 0%, rgba(63,182,139,0.05) 100%);
border: 1px solid rgba(63,182,139,0.3);
border-radius: 20px;
font-size: 12px;
}
.provider-icon {
font-size: 16px;
}
.provider-name {
font-weight: 600;
}
.provider-check {
color: var(--accent);
font-size: 11px;
}
/* Strategy Cards */
.recommended-strategy {
margin-bottom: 16px;
}
.strategy-card {
background: var(--bg-elevated);
border: 2px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-card:hover {
border-color: rgba(63,182,139,0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.strategy-card.selected {
border-color: var(--accent);
background: linear-gradient(135deg, var(--accent-dim) 0%, transparent 100%);
}
.strategy-card.recommended {
border-color: var(--accent);
background: linear-gradient(135deg, rgba(63,182,139,0.12) 0%, rgba(63,182,139,0.03) 100%);
}
.strategy-card.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.strategy-card-header {
margin-bottom: 12px;
}
.strategy-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
font-weight: 700;
margin-bottom: 8px;
}
.recommended-badge {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
color: white;
}
.disabled-badge {
background: var(--danger-dim);
color: var(--danger);
}
.strategy-card .strategy-title {
font-size: 16px;
font-weight: 700;
}
/* Strategy Diagram */
.strategy-diagram {
margin: 16px 0;
padding: 16px;
background: var(--bg);
border-radius: var(--radius-md);
overflow-x: auto;
}
.strategy-diagram.mini {
padding: 10px;
margin: 10px 0;
}
.strategy-diagram.mini .diagram-email {
font-size: 9px;
}
.diagram-flow {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
min-width: fit-content;
}
.diagram-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
min-width: 120px;
}
.diagram-step.generated {
border-color: rgba(63,182,139,0.3);
background: linear-gradient(135deg, rgba(63,182,139,0.1) 0%, transparent 100%);
}
.diagram-step.aws {
background: linear-gradient(135deg, rgba(255,153,0,0.15) 0%, transparent 100%);
border-color: rgba(255,153,0,0.3);
}
.diagram-step.inbox {
background: linear-gradient(135deg, rgba(66,133,244,0.15) 0%, transparent 100%);
border-color: rgba(66,133,244,0.3);
}
.diagram-label {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
}
.diagram-icon {
font-size: 24px;
}
.diagram-email {
font-size: 10px;
font-family: var(--vscode-editor-font-family, monospace);
color: var(--fg);
white-space: nowrap;
}
.diagram-email .highlight {
color: var(--accent);
font-weight: 700;
}
.diagram-arrow {
font-size: 20px;
color: var(--muted);
flex-shrink: 0;
}
/* Strategy Features */
.strategy-features {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.strategy-features.compact {
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
.strategy-pros, .strategy-cons {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.feature-item {
font-size: 11px;
padding: 4px 0;
}
.feature-item.pro {
color: var(--accent);
}
.feature-item.con {
color: var(--warning);
}
.strategy-select-btn {
margin-top: 8px;
}
.btn-block {
width: 100%;
}
.disabled-reason {
font-size: 10px;
color: var(--muted);
text-align: center;
padding: 8px;
background: var(--bg);
border-radius: var(--radius-sm);
margin-top: 8px;
}
/* Other Strategies Toggle */
.other-strategies {
margin-top: 12px;
}
.other-strategies-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 12px;
color: var(--muted);
transition: all 0.2s ease;
}
.other-strategies-toggle:hover {
background: var(--glass-bg);
color: var(--fg);
}
.other-strategies[open] .toggle-icon {
transform: rotate(180deg);
}
.toggle-icon {
transition: transform 0.2s ease;
}
.toggle-icon svg {
width: 14px;
height: 14px;
}
.strategies-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 12px;
}
/* IMAP Settings in Wizard */
.imap-settings {
display: flex;
flex-direction: column;
gap: 12px;
}
.connection-status {
min-height: 24px;
font-size: 11px;
margin-top: 8px;
}
.connection-status.success {
color: var(--accent);
}
.connection-status.error {
color: var(--danger);
}
/* Pool Stats */
.pool-stats {
display: flex;
gap: 16px;
padding: 10px 12px;
background: var(--bg);
border-bottom: 1px solid var(--border);
font-size: 11px;
}
.pool-count {
color: var(--accent);
}
.pool-pending {
color: var(--muted);
}
/* Optional label */
.optional {
font-weight: 400;
color: var(--muted);
font-size: 10px;
}
/* Button icon */
.btn-icon {
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body data-lang="en">
<div class="app">
<div class="header">
<div class="header-left">
<span class="header-title">KIRO</span>
<span class="header-badge">0/0</span>
<span class="patch-indicator" id="patchIndicator" title="Kiro Patch"></span>
</div>
<div class="header-actions">
<button class="icon-btn" onclick="refresh()" title="Reload accounts and update token status"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.5 2v5h-5l1.8-1.8A4.5 4.5 0 1 0 12.5 8h1a5.5 5.5 0 1 1-1.6-3.9L13.5 2z"/></svg></button>
<button class="icon-btn" onclick="toggleLogs()" title="Console"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9 1H3v14h10V5l-4-4zm3 13H4V2h4v4h4v8z"/></svg></button>
</div>
</div>
<div class="hero empty" onclick="openSettings()">
<span class="hero-label">No active</span>
</div>
<div class="toolbar compact">
<div class="toolbar-row">
<!-- Primary buttons - always visible -->
<div class="toolbar-buttons toolbar-primary">
<button class="btn btn-primary btn-icon" onclick="startQuickAutoReg()" title="Add Account"></button>
<button class="btn btn-secondary btn-icon" onclick="toggleSelectionMode()" title="Select" id="selectModeBtn">☑️</button>
</div>
<!-- Secondary buttons - hidden on narrow screens, moved to dropdown -->
<div class="toolbar-buttons toolbar-secondary">
<button class="btn btn-secondary btn-icon" onclick="openSsoModal()" title="SSO Import">🌐</button>
<button class="btn btn-secondary btn-icon" onclick="checkAllAccountsHealth()" title="Check Health">🩺</button>
</div>
<!-- More dropdown - visible only on narrow screens -->
<div class="toolbar-more-wrapper">
<button class="btn btn-secondary btn-icon toolbar-more-btn" onclick="toggleToolbarMore()" title="More"></button>
<div class="toolbar-more-menu" id="toolbarMoreMenu">
<button class="toolbar-more-item" onclick="openSsoModal(); toggleToolbarMore();">
<span class="more-icon">🌐</span>
<span class="more-label">SSO Import</span>
</button>
<button class="toolbar-more-item" onclick="checkAllAccountsHealth(); toggleToolbarMore();">
<span class="more-icon">🩺</span>
<span class="more-label">Check Health</span>
</button>
<div class="toolbar-more-divider"></div>
<button class="toolbar-more-item" onclick="exportAllAccounts(); toggleToolbarMore();">
<span class="more-icon">📤</span>
<span class="more-label">Export</span>
</button>
</div>
</div>
<div class="search-wrapper">
<span class="search-icon"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.7 10.3a6 6 0 1 0-1.4 1.4l4 4 1.4-1.4-4-4zM6 10a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></svg></span>
<input type="text" class="search-input" id="searchInput" placeholder="Search accounts..." oninput="searchAccounts(this.value)">
<button class="search-clear" onclick="clearSearch()">×</button>
</div>
<select class="filter-select" id="tokenFilterSelect" onchange="filterByTokens(this.value)">
<option value="all">All</option>
<option value="fresh">🟢 Fresh</option>
<option value="partial">🟡 Partial</option>
<option value="trial">🔵 Trial</option>
<option value="empty">⚫ Empty</option>
</select>
</div>
<div class="bulk-actions-bar hidden" id="bulkActionsBar">
<div class="bulk-info">
<span class="bulk-count" id="bulkCount">0</span> selected
</div>
<div class="bulk-buttons">
<button class="btn btn-secondary btn-sm" onclick="selectAllAccounts()" title="Select All">☑️</button>
<button class="btn btn-secondary btn-sm" onclick="deselectAllAccounts()" title="Deselect All"></button>
<button class="btn btn-secondary btn-sm" onclick="exportSelectedAccounts()" title="Export">📤</button>
<button class="btn btn-secondary btn-sm" onclick="refreshSelectedTokens()" title="Refresh">🔄</button>
<button class="btn btn-danger btn-sm" onclick="deleteSelectedAccounts()" title="Delete">🗑️</button>
</div>
<button class="btn btn-secondary btn-sm" onclick="toggleSelectionMode()"></button>
</div>
</div>
<div class="list" id="accountList">
<div class="loading">Loading accounts...</div>
</div>
<div class="console-drawer " id="logsDrawer">
<div class="console-header" onclick="toggleLogs()">
<div class="console-header-left">
<span class="console-title-group">
<span class="console-icon-indicator "></span>
<span class="console-title">Console</span>
<span class="console-badge " id="logsCount">0</span>
</span>
<span class="console-new-indicator" id="newLogsIndicator">● New</span>
</div>
<div class="console-header-right">
<span class="console-toggle-icon"></span>
</div>
</div>
<div class="console-toolbar">
<div class="console-filters">
<button class="console-filter active" data-filter="all" onclick="filterConsole('all')" title="All logs">
<span class="filter-label">All</span>
<span class="filter-count">0</span>
</button>
<button class="console-filter " data-filter="error" onclick="filterConsole('error')" title="Errors">
<span class="filter-icon error"></span>
</button>
<button class="console-filter " data-filter="warning" onclick="filterConsole('warning')" title="Warnings">
<span class="filter-icon warning">!</span>
</button>
<button class="console-filter " data-filter="success" onclick="filterConsole('success')" title="Success">
<span class="filter-icon success"></span>
</button>
</div>
<div class="console-actions">
<button class="console-btn scroll-btn" onclick="scrollConsoleToBottom()" title="Scroll to bottom" id="scrollToBottomBtn">
<span></span>
</button>
<button class="console-btn" onclick="clearConsole()" title="Clear console output">
<span>🗑</span>
</button>
<button class="console-btn" onclick="copyLogs()" title="Copy logs to clipboard">
<span>📋</span>
</button>
</div>
</div>
<div class="console-body" id="logsContent" onscroll="handleConsoleScroll()"></div>
<div class="console-new-messages" id="newMessagesBar" onclick="scrollConsoleToBottom()">
<span>↓ New messages below</span>
</div>
</div>
<div class="overlay" id="settingsOverlay">
<div class="overlay-content">
<div class="overlay-header">
<button class="overlay-back" onclick="closeSettings()">← Back</button>
<span class="overlay-title">Settings</span>
</div>
<div class="overlay-body">
<!-- Stats Section (embedded) -->
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this)">
<span class="settings-card-icon">📊</span>
<span class="settings-card-title">Statistics</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="stats-dashboard">
<div class="stats-header">
<h3 class="stats-title">📊 Statistics</h3>
</div>
<!-- Summary Cards -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-value">0</div>
<div class="stat-label">total</div>
</div>
<div class="stat-card success">
<div class="stat-value">0</div>
<div class="stat-label">Active</div>
</div>
<div class="stat-card danger">
<div class="stat-value">0</div>
<div class="stat-label">BANNED</div>
</div>
<div class="stat-card warning">
<div class="stat-value">0</div>
<div class="stat-label">expired</div>
</div>
</div>
<!-- Usage Overview -->
<div class="stats-section">
<div class="stats-section-title">Today's Usage</div>
<div class="usage-overview">
<div class="usage-bar-container">
<div class="usage-bar-fill" style="width: 0%"></div>
</div>
<div class="usage-numbers">
<span class="usage-current">0</span>
<span class="usage-separator">/</span>
<span class="usage-limit">0</span>
</div>
</div>
<div class="usage-avg">
Avg per account: <strong>0</strong>
</div>
</div>
<!-- Mini Chart -->
<div class="stats-section">
<div class="stats-section-title">Weekly Usage</div>
<div class="mini-chart">
<div class="chart-bar" style="height: 30%"></div>
<div class="chart-bar" style="height: 45%"></div>
<div class="chart-bar" style="height: 60%"></div>
<div class="chart-bar" style="height: 80%"></div>
<div class="chart-bar" style="height: 55%"></div>
<div class="chart-bar" style="height: 40%"></div>
<div class="chart-bar" style="height: 25%"></div>
</div>
<div class="chart-labels">
<span>Mon</span>
<span>Tue</span>
<span>Wed</span>
<span>Thu</span>
<span>Fri</span>
<span>Sat</span>
<span>Sun</span>
</div>
</div>
<!-- Account Health -->
<div class="stats-section">
<div class="stats-section-title">Account Health</div>
<div class="health-bars">
<div class="health-row">
<span class="health-label">✅ Active</span>
<div class="health-bar">
<div class="health-fill success" style="width: 0%"></div>
</div>
<span class="health-percent">0%</span>
</div>
<div class="health-row">
<span class="health-label">⛔ BANNED</span>
<div class="health-bar">
<div class="health-fill danger" style="width: 0%"></div>
</div>
<span class="health-percent">0%</span>
</div>
<div class="health-row">
<span class="health-label">⏰ expired</span>
<div class="health-bar">
<div class="health-fill warning" style="width: 0%"></div>
</div>
<span class="health-percent">0%</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Active Profile Card -->
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this)">
<span class="settings-card-icon">📧</span>
<span class="settings-card-title">Active Profile</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="active-profile-content" id="activeProfileContent">
<div class="active-profile-empty">
<span class="empty-text">No profile configured</span>
<button class="btn btn-primary btn-sm" onclick="switchTab('profiles')">Configure</button>
</div>
</div>
</div>
</div>
<!-- Automation Card -->
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this)">
<span class="settings-card-icon">⚙️</span>
<span class="settings-card-title">Automation</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="setting-row">
<div>
<div class="setting-label">Auto-switch on expiry</div>
<div class="setting-desc">Automatically switch to next valid account</div>
</div>
<label class="toggle">
<input type="checkbox" onchange="toggleAutoSwitch(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row" id="autoSwitchThresholdRow" style="display:none">
<div>
<div class="setting-label">Switch threshold</div>
<div class="setting-desc">Switch when remaining < this value</div>
</div>
<input type="number" class="input-number" id="autoSwitchThreshold"
value="10"
min="1" max="490" step="10"
onchange="updateSetting('autoSwitchThreshold', parseInt(this.value))">
</div>
<div class="setting-row">
<div>
<div class="setting-label">Headless mode</div>
<div class="setting-desc">Run browser in background</div>
</div>
<label class="toggle">
<input type="checkbox" onchange="toggleSetting('headless', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Device Flow auth</div>
<div class="setting-desc">Use device code instead of local server</div>
</div>
<label class="toggle">
<input type="checkbox" onchange="toggleSetting('deviceFlow', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Registration Strategy Card (Anti-Ban) -->
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this)">
<span class="settings-card-icon">🛡️</span>
<span class="settings-card-title">Registration Strategy</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="setting-desc" style="margin-bottom: 16px;">Choose how to register accounts (affects ban risk)</div>
<!-- WebView Strategy (Safe) -->
<div class="strategy-option strategy-safe " onclick="selectRegistrationStrategy('webview')">
<div class="strategy-icon-wrapper strategy-icon-safe">
<span class="strategy-icon">🌐</span>
</div>
<input type="radio" name="strategy" value="webview" checked style="display: none;">
<div class="strategy-content">
<div class="strategy-header">
<strong>WebView (Recommended)</strong>
<span class="badge badge-success">✓ Low risk</span>
</div>
<div class="strategy-desc">Opens real browser, you enter credentials manually. Minimal ban risk.</div>
<div class="strategy-features">
<div class="strategy-feature feature-pro">
<span class="feature-icon"></span>
<span>Real browser fingerprint</span>
</div>
<div class="strategy-feature feature-pro">
<span class="feature-icon"></span>
<span>Human-like behavior</span>
</div>
<div class="strategy-feature feature-con">
<span class="feature-icon">!</span>
<span>Manual input required</span>
</div>
</div>
<div class="strategy-risk-bar">
<div class="risk-label">Ban risk:</div>
<div class="risk-meter">
<div class="risk-fill risk-low" style="width: 10%;"></div>
</div>
<div class="risk-value risk-low-text">&lt;10%</div>
</div>
</div>
</div>
<!-- Automated Strategy (Risky) -->
<div class="strategy-option strategy-risky selected" onclick="selectRegistrationStrategy('automated')">
<div class="strategy-icon-wrapper strategy-icon-risky">
<span class="strategy-icon">🤖</span>
</div>
<input type="radio" name="strategy" value="automated" style="display: none;">
<div class="strategy-content">
<div class="strategy-header">
<strong>Automated (Legacy)</strong>
<span class="badge badge-danger">⚠ High risk</span>
</div>
<div class="strategy-desc">Automated browser with DrissionPage. Works for some users.</div>
<div class="strategy-features">
<div class="strategy-feature feature-pro">
<span class="feature-icon"></span>
<span>Fully automatic</span>
</div>
<div class="strategy-feature feature-con">
<span class="feature-icon"></span>
<span>High detection risk</span>
</div>
</div>
<div class="strategy-risk-bar">
<div class="risk-label">Ban risk:</div>
<div class="risk-meter">
<div class="risk-fill risk-high" style="width: 70%;"></div>
</div>
<div class="risk-value risk-high-text">40-90%</div>
</div>
</div>
</div>
<!-- Defer Quota Check (only for Automated) -->
<div id="deferQuotaCheckOption" class="setting-row" style="margin-top: 16px; display: none;">
<div>
<div class="setting-label">Defer quota check</div>
<div class="setting-desc">Do NOT check quota immediately after registration (reduces ban risk)</div>
</div>
<label class="toggle">
<input type="checkbox" checked onchange="toggleSetting('deferQuotaCheck', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
<!-- OAuth Provider (only for WebView) -->
<div id="oauthProviderOption" class="setting-row" style="margin-top: 16px; ">
<div>
<div class="setting-label">OAuth Provider</div>
<div class="setting-desc">Choose provider for WebView registration</div>
</div>
<select class="select" id="oauthProviderSelect" onchange="updateSetting('oauthProvider', this.value)">
<option value="ask" selected>Ask every time</option>
<option value="Google" >Google</option>
<option value="Github" >GitHub</option>
</select>
</div>
</div>
</div>
<!-- Interface Card -->
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this)">
<span class="settings-card-icon">🎨</span>
<span class="settings-card-title">Interface</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="setting-row">
<div>
<div class="setting-label">Language</div>
<div class="setting-desc">Interface language</div>
</div>
<select class="select" onchange="changeLanguage(this.value)"><option value="en" selected>EN</option><option value="ru" >RU</option><option value="zh" >ZH</option><option value="es" >ES</option><option value="pt" >PT</option><option value="ja" >JA</option><option value="de" >DE</option><option value="fr" >FR</option><option value="ko" >KO</option><option value="hi" >HI</option></select>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Verbose logging</div>
<div class="setting-desc">Show detailed logs</div>
</div>
<label class="toggle">
<input type="checkbox" onchange="toggleSetting('verbose', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Screenshots on error</div>
<div class="setting-desc">Save screenshots when errors occur</div>
</div>
<label class="toggle">
<input type="checkbox" onchange="toggleSetting('screenshotsOnError', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this, event)">
<span class="settings-card-icon">🌐</span>
<span class="settings-card-title">Proxy</span>
<div class="proxy-status-badge proxy-status-unknown">
<span></span>
<span>Proxy not configured</span>
</div>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="setting-desc" style="margin-bottom: 12px;">Route browser traffic through proxy server</div>
<!-- Proxy Address Input -->
<div class="form-group">
<label class="form-label">Proxy Address</label>
<div class="proxy-input-row">
<input
type="text"
class="form-input proxy-input"
id="proxyAddressInput"
placeholder="http://user:pass@host:port"
value=""
onchange="updateProxyAddress(this.value)"
/>
<button
class="btn btn-secondary"
id="testProxyBtn"
onclick="testProxy(); event.stopPropagation()"
disabled
>
🔍 Test Proxy
</button>
</div>
<div class="form-hint">Formats: host:port:user:pass, socks5://user:pass@host:port</div>
</div>
<!-- Use Proxy for Registration -->
<div class="setting-row" style="margin-top: 16px;">
<div>
<div class="setting-label">Use proxy for registration</div>
<div class="setting-desc">Route registration traffic through proxy</div>
</div>
<label class="toggle" onclick="event.stopPropagation()">
<input
type="checkbox"
id="useProxyToggle"
disabled
onchange="toggleSetting('useProxyForRegistration', this.checked)"
>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this, event)">
<span class="settings-card-icon">🛡️</span>
<span class="settings-card-title">Spoofing mode</span>
<label class="toggle" onclick="event.stopPropagation()">
<input type="checkbox" id="spoofingToggle" checked onchange="toggleSpoofing(this.checked)">
<span class="toggle-slider"></span>
</label>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="setting-desc" style="margin-bottom: 12px;">Randomize browser fingerprint</div>
<div class="spoof-details" id="spoofDetails" style="margin-top: 0;">
<div class="spoof-modules">
<div class="spoof-module">
<span class="module-icon">🤖</span>
<div class="module-info">
<div class="module-name">Automation</div>
<div class="module-desc">Hides webdriver flags</div>
</div>
</div>
<div class="spoof-module">
<span class="module-icon">🎨</span>
<div class="module-info">
<div class="module-name">Canvas/WebGL</div>
<div class="module-desc">Randomizes fingerprint</div>
</div>
</div>
<div class="spoof-module">
<span class="module-icon">🖥️</span>
<div class="module-info">
<div class="module-name">Navigator</div>
<div class="module-desc">Spoofs hardware info</div>
</div>
</div>
<div class="spoof-module">
<span class="module-icon">🔊</span>
<div class="module-info">
<div class="module-name">Audio/Fonts</div>
<div class="module-desc">Masks audio context</div>
</div>
</div>
<div class="spoof-module">
<span class="module-icon">🌐</span>
<div class="module-info">
<div class="module-name">WebRTC</div>
<div class="module-desc">Hides local IP</div>
</div>
</div>
<div class="spoof-module">
<span class="module-icon">🖱️</span>
<div class="module-info">
<div class="module-name">Behavior</div>
<div class="module-desc">Human-like actions</div>
</div>
</div>
</div>
<div class="spoof-warning">
<span>⚠️</span>
<span>Disabling may cause bot detection failures</span>
</div>
</div>
</div>
</div>
<div class="settings-card collapsed">
<div class="settings-card-header" onclick="toggleSettingsCard(this, event)">
<span class="settings-card-icon">📦</span>
<span class="settings-card-title">Export Accounts</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="section-desc" style="margin-bottom: 12px;">Export accounts with tokens for transfer</div>
<div class="import-export-actions">
<button class="btn btn-primary btn-full" onclick="exportAllAccounts()">
📤 Export All
</button>
<button class="btn btn-secondary btn-full" onclick="importAccounts()">
📥 Import Accounts
</button>
</div>
</div>
</div>
<div class="settings-card collapsed danger-card">
<div class="settings-card-header" onclick="toggleSettingsCard(this, event)">
<span class="settings-card-icon">⚠️</span>
<span class="settings-card-title">Danger Zone</span>
<span class="settings-card-toggle"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M10.3 2.3L4.6 8l5.7 5.7.7-.7L6 8l5-5-.7-.7z"/></svg></span>
</div>
<div class="settings-card-body">
<div class="danger-zone-card patch-card">
<div class="danger-zone-info">
<div class="danger-zone-label">Kiro Patch</div>
<div class="danger-zone-desc">Patches Kiro to use custom Machine ID</div>
<div class="patch-status-row">
<span id="patchStatusText" class="patch-status">Loading...</span>
<span id="patchVersionInfo" class="patch-version-info"></span>
</div>
<div class="patch-machine-id-row">
<span id="currentMachineId" class="machine-id-preview"></span>
</div>
</div>
<div class="danger-zone-actions">
<button id="patchKiroBtn" class="btn btn-warning btn-sm" onclick="confirmPatchKiro(); event.stopPropagation()" title="Patch Kiro">
🔧 Patch
</button>
<button id="updatePatchBtn" class="btn btn-primary btn-sm" onclick="confirmPatchKiro(); event.stopPropagation()" style="display:none" title="Update available">
⬆️ Update
</button>
<button id="unpatchKiroBtn" class="btn btn-secondary btn-sm" onclick="confirmUnpatchKiro(); event.stopPropagation()" style="display:none" title="Remove Patch">
↩️ Remove
</button>
<button id="generateIdBtn" class="btn btn-secondary btn-sm" onclick="generateNewMachineId(); event.stopPropagation()" title="New ID">
🎲 New ID
</button>
</div>
</div>
<div class="danger-zone-card">
<div class="danger-zone-info">
<div class="danger-zone-label">Reset Machine ID</div>
<div class="danger-zone-desc">Generate new telemetry IDs for Kiro. Use if account is banned.</div>
</div>
<button class="btn btn-danger btn-sm" onclick="confirmResetMachineId(); event.stopPropagation()" title="Reset machineId, sqmId, devDeviceId and serviceMachineId">
🔄 Reset
</button>
</div>
<div class="danger-zone-hint">
💡 Restart Kiro after reset
</div>
</div>
</div>
<div class="settings-footer">
<span class="settings-version">vstandalone</span>
<button class="btn btn-secondary" onclick="openUpdateUrl('https://t.me/whitebite_devsoft')" title="Telegram">📢 TG</button>
<button class="btn btn-secondary" onclick="checkUpdates()">Check Updates</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="ssoModal" onclick="if(event.target === this) closeSsoModal()">
<div class="modal">
<div class="modal-header">
<span class="modal-title">SSO Import</span>
<button class="modal-close" onclick="closeSsoModal()">×</button>
</div>
<div class="modal-body">
<div class="modal-hint">1. Open view.awsapps.com/start
2. DevTools → Application → Cookies
3. Copy x-amz-sso_authn</div>
<textarea class="modal-textarea" id="ssoTokenInput" placeholder="Paste cookie..."></textarea>
<button class="btn btn-primary" style="width:100%" onclick="importSsoToken()">Import</button>
</div>
</div>
</div>
<div class="modal-overlay" id="accountSecretsModal" onclick="if(event.target === this) closeAccountSecrets()">
<div class="modal">
<div class="modal-header">
<span class="modal-title">Account Secrets</span>
<button class="modal-close" onclick="closeAccountSecrets()">ЎБ</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Email</label>
<textarea class="modal-textarea" id="accountSecretEmail" readonly></textarea>
<button class="btn btn-secondary" style="width:100%" onclick="copySecretField('accountSecretEmail')">Copy</button>
</div>
<div class="form-group">
<label class="form-label">RefreshToken</label>
<textarea class="modal-textarea" id="accountSecretRefreshToken" readonly></textarea>
<button class="btn btn-secondary" style="width:100%" onclick="copySecretField('accountSecretRefreshToken')">Copy</button>
</div>
<div class="form-group">
<label class="form-label">Client ID</label>
<textarea class="modal-textarea" id="accountSecretClientId" readonly></textarea>
<button class="btn btn-secondary" style="width:100%" onclick="copySecretField('accountSecretClientId')">Copy</button>
</div>
<div class="form-group">
<label class="form-label">Client Secret</label>
<textarea class="modal-textarea" id="accountSecretClientSecret" readonly></textarea>
<button class="btn btn-secondary" style="width:100%" onclick="copySecretField('accountSecretClientSecret')">Copy</button>
</div>
</div>
</div>
</div>
<div class="dialog-overlay" id="dialogOverlay" onclick="if(event.target === this) closeDialog()">
<div class="dialog">
<div class="dialog-title" id="dialogTitle">Delete Account</div>
<div class="dialog-text" id="dialogText">Are you sure you want to delete this account?</div>
<div class="dialog-actions">
<button class="btn btn-secondary" onclick="closeDialog()">Cancel</button>
<button class="btn btn-danger" id="dialogConfirmBtn" onclick="dialogAction()">Delete</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<div class="profiles-panel" id="profilesPanel">
<div class="profiles-panel-header">
<button class="overlay-back" onclick="closeProfilesPanel()">← Back</button>
<span class="profiles-panel-title">Email Profiles</span>
</div>
<div class="profiles-panel-content" id="profilesContent">
<div class="profiles-empty">
<div class="empty-icon">📧</div>
<div class="empty-text">No profiles configured</div>
<button class="btn btn-primary" onclick="createProfile()">
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M7 7V1h2v6h6v2H9v6H7V9H1V7h6z"/></svg> Add Profile
</button>
</div>
</div>
</div>
<div class="profile-editor" id="profileEditor">
<div class="editor-header">
<button class="overlay-back" onclick="closeProfileEditor()">← Back</button>
<span class="editor-title">New Profile</span>
</div>
<div class="editor-content">
<div class="form-group">
<label class="form-label">Profile Name</label>
<input type="text" class="form-input" id="profileName" placeholder="My Gmail">
</div>
<div class="form-section" id="imapSection">
<div class="form-section-title">IMAP</div>
<div class="form-group" id="imapEmailGroup">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="imapUser" placeholder="your@email.com" oninput="onEmailInput(this.value)">
<div class="form-hint" id="providerHint"></div>
</div>
<div class="form-row">
<div class="form-group flex-2">
<label class="form-label">Server</label>
<input type="text" class="form-input" id="imapServer" placeholder="imap.gmail.com">
</div>
<div class="form-group flex-1">
<label class="form-label">Port</label>
<input type="number" class="form-input" id="imapPort" value="993">
</div>
</div>
<div class="form-group" id="imapPasswordGroup">
<label class="form-label">Password</label>
<div class="password-input-wrapper">
<input type="password" class="form-input" id="imapPassword" placeholder="••••••••">
<button class="password-toggle" onclick="togglePasswordVisibility('imapPassword')"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 4C4.5 4 1.6 6.5.5 8c1.1 1.5 4 4 7.5 4s6.4-2.5 7.5-4c-1.1-1.5-4-4-7.5-4zm0 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg></button>
</div>
</div>
<button class="btn btn-secondary" id="testConnectionBtn" onclick="testImapConnection()">🔌 Test</button>
</div>
<div class="form-section" id="proxySection">
<div class="form-section-title">Proxy</div>
<div class="form-section-desc">Route browser traffic through proxy server</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="proxyEnabled" onchange="toggleProxyFields()">
<span class="toggle-text">Use Proxy</span>
</label>
</div>
<div class="form-group proxy-fields" id="proxyFields" style="display: none;">
<label class="form-label">Proxy URL</label>
<textarea class="form-input form-textarea" id="proxyUrls" rows="4" placeholder="socks5://user:pass@host:port"></textarea>
<div class="form-hint">Formats: host:port:user:pass, socks5://user:pass@host:port</div>
<div class="proxy-stats" id="proxyStats"></div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">Email Strategy</div>
<div class="form-section-desc">Choose how to generate emails for registration</div>
<div class="strategy-selector">
<div class="strategy-option selected" data-strategy="single" onclick="selectStrategy('single')">
<div class="strategy-icon">📧</div>
<div class="strategy-content">
<div class="strategy-label">Single Email</div>
<div class="strategy-desc">Uses your IMAP email directly. Only 1 account per email.</div>
<div class="strategy-example">Example: your@gmail.com → your@gmail.com</div>
</div>
<div class="strategy-check"></div>
</div>
<div class="strategy-option" data-strategy="plus_alias" onclick="selectStrategy('plus_alias')">
<div class="strategy-icon"></div>
<div class="strategy-content">
<div class="strategy-label">Plus Alias</div>
<div class="strategy-desc">Adds +random to your email. All emails arrive to same inbox. Works with Gmail, Outlook, Yandex.</div>
<div class="strategy-example">Example: your@gmail.com → your+kiro5x7@gmail.com</div>
</div>
<div class="strategy-check"></div>
</div>
<div class="strategy-option" data-strategy="catch_all" onclick="selectStrategy('catch_all')">
<div class="strategy-icon">🌐</div>
<div class="strategy-content">
<div class="strategy-label">Catch-All Domain</div>
<div class="strategy-desc">Generates random emails on your domain. Requires catch-all configured on mail server.</div>
<div class="strategy-example">Example: JohnSmith4521@yourdomain.com</div>
</div>
<div class="strategy-check"></div>
</div>
<div class="strategy-option" data-strategy="pool" onclick="selectStrategy('pool')">
<div class="strategy-icon">📋</div>
<div class="strategy-content">
<div class="strategy-label">Email Pool</div>
<div class="strategy-desc">Use a list of your email addresses. Each email is used once in order.</div>
</div>
<div class="strategy-check"></div>
</div>
</div>
<div class="strategy-config" id="catchAllConfig" style="display: none;">
<div class="config-hint">Enter domain with catch-all configured. All emails to any@domain will arrive to your IMAP inbox.</div>
<div class="form-group">
<label class="form-label">Registration Domain</label>
<input type="text" class="form-input" id="catchAllDomain" placeholder="your-domain.com">
</div>
</div>
<div class="strategy-config" id="poolConfig" style="display: none;">
<div class="config-hint">Add email addresses to use in order. Each email = 1 account. Format: email:password</div>
<div class="email-pool-editor">
<div class="pool-stats" id="poolStats"></div>
<div class="pool-list" id="poolList"></div>
<div class="pool-add">
<input type="text" class="form-input" id="newPoolEmail" placeholder="email:password or email..." onkeypress="if(event.key==='Enter') addEmailToPool()" onpaste="handlePoolPaste(event)">
<button class="btn btn-secondary" onclick="addEmailToPool()"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M7 7V1h2v6h6v2H9v6H7V9H1V7h6z"/></svg></button>
</div>
<div class="pool-actions">
<button class="btn btn-secondary" onclick="importEmailsFromFile()">📁 From file</button>
<button class="btn btn-secondary" onclick="pasteEmails()">📋 Paste</button>
</div>
</div>
</div>
</div>
</div>
<div class="editor-footer">
<button class="btn btn-secondary" onclick="closeProfileEditor()">Cancel</button>
<button class="btn btn-primary" onclick="saveProfile()">Save</button>
</div>
</div>
</div>
<div id="toastContainer" class="toast-container"></div>
<script>
// === WebSocket Adapter for Standalone ===
// Replaces vscode.postMessage with WebSocket
let ws = null;
let reconnectAttempts = 0;
const MAX_RECONNECT = 5;
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
ws.onopen = () => {
console.log('[WS] Connected');
reconnectAttempts = 0;
// Initial data load
vscode.postMessage({ command: 'refresh' });
vscode.postMessage({ command: 'getPatchStatus' });
vscode.postMessage({ command: 'getActiveProfile' });
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
// Dispatch to window.postMessage so extension script handles it
window.dispatchEvent(new MessageEvent('message', { data: msg }));
} catch (e) {
console.error('[WS] Parse error:', e);
}
};
ws.onclose = () => {
console.log('[WS] Disconnected');
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = (error) => {
console.error('[WS] Error:', error);
};
}
// Mock vscode API - sends via WebSocket instead
const vscode = {
postMessage: (msg) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
} else {
console.warn('[WS] Not connected, queuing:', msg.command);
}
},
getState: () => {
try {
return JSON.parse(localStorage.getItem('kiro-state') || '{}');
} catch { return {}; }
},
setState: (state) => {
localStorage.setItem('kiro-state', JSON.stringify(state));
}
};
// Override acquireVsCodeApi to return our mock
function acquireVsCodeApi() {
return vscode;
}
// Connect on load
document.addEventListener('DOMContentLoaded', connectWebSocket);
const T = {"kiroAccounts":"Kiro Accounts","accounts":"Accounts","registration":"Registration","llm":"AI/LLM","profiles":"Profiles","settings":"Settings","compactViewTip":"Shrink cards to show more accounts","settingsTip":"Configure auto-switch, browser mode, logging","back":"Back","valid":"valid","expired":"expired","total":"total","noActive":"No active","validFilter":"Valid","expiredFilter":"Expired","activeGroup":"Active","readyGroup":"Ready","badGroup":"Expired / Exhausted","expiredGroup":"Expired","exhaustedGroup":"Exhausted","bannedGroup":"Banned","banned":"BANNED","refreshAll":"Refresh all","deleteAll":"Delete all","todaysUsage":"Today's Usage","used":"used","daysLeft":"days left","remaining":"remaining","resetsAtMidnight":"Resets at midnight","autoReg":"Auto-Register","autoRegTip":"Start automatic account registration","autoRegCountLabel":"Count","autoRegCountPlaceholder":"Number of accounts","import":"Import","importTip":"Import existing token from JSON file","refresh":"Refresh","refreshTip":"Reload accounts and update token status","export":"Export","exportTip":"Save all accounts to JSON file","running":"Running...","stop":"Stop","all":"All","byUsage":"By Usage","byExpiry":"By Expiry","byDate":"By Date","searchPlaceholder":"Search accounts...","newBadge":"NEW","filterByTokens":"Filter","fresh":"Fresh","partial":"Partial","trial":"Trial","empty":"Empty","more":"More","active":"Active","copyTokenTip":"Copy access token to clipboard","refreshTokenTip":"Refresh expired token","viewQuotaTip":"Show usage statistics for this account","deleteTip":"Remove account and token file","noAccounts":"No accounts yet","createFirst":"Create First Account","console":"Console","clearTip":"Clear console output","openLogTip":"Open full log file in editor","copyLogsTip":"Copy logs to clipboard","scrollToBottom":"Scroll to bottom","newMessagesBelow":"New messages below","step":"Step","connected":"Connected","confirm":"Confirm","apply":"Apply","cancel":"Cancel","deleteTitle":"Delete Account","deleteConfirm":"Are you sure you want to delete this account?","areYouSure":"Are you sure?","settingsTitle":"Settings","autoSwitch":"Auto-switch on expiry","autoSwitchDesc":"Automatically switch to next valid account","hideExhausted":"Hide exhausted","hideExhaustedDesc":"Hide accounts with 100% usage","headless":"Headless mode","headlessDesc":"Run browser in background","verbose":"Verbose logging","verboseDesc":"Show detailed logs","screenshots":"Screenshots on error","screenshotsDesc":"Save screenshots when errors occur","spoofing":"Spoofing mode","spoofingDesc":"Randomize browser fingerprint","deviceFlow":"Device Flow auth","deviceFlowDesc":"Use device code instead of local server","language":"Language","languageDesc":"Interface language","spoofAutomation":"Automation","spoofAutomationDesc":"Hides webdriver flags","spoofCanvas":"Canvas/WebGL","spoofCanvasDesc":"Randomizes fingerprint","spoofNavigator":"Navigator","spoofNavigatorDesc":"Spoofs hardware info","spoofAudio":"Audio/Fonts","spoofAudioDesc":"Masks audio context","spoofWebrtc":"WebRTC","spoofWebrtcDesc":"Hides local IP","spoofBehavior":"Behavior","spoofBehaviorDesc":"Human-like actions","spoofWarning":"Disabling may cause bot detection failures","kiroPatch":"Kiro Patch","kiroPatchDesc":"Patches Kiro to use custom Machine ID","patchStatusLoading":"Loading...","patchStatusActive":"Patched","patchStatusNotPatched":"Not patched","patchStatusOutdated":"Outdated","patchVersion":"Patch","patchVersionInstalled":"installed","patchVersionLatest":"latest","patchUpdateAvailable":"Update available","kiroVersion":"Kiro","patch":"Patch","removePatch":"Remove","updatePatch":"Update","newMachineId":"New ID","patchKiroTitle":"Patch Kiro","patchKiroConfirm":"This will patch Kiro to use custom Machine ID. Restart Kiro after patching. Continue?","removePatchTitle":"Remove Patch","removePatchConfirm":"This will restore original Kiro files. Continue?","newProfile":"New Profile","editProfile":"Edit Profile","profileName":"Profile Name","profileNamePlaceholder":"My Gmail","server":"Server","port":"Port","password":"Password","testConnection":"Test","proxy":"Proxy","proxyEnabled":"Use Proxy","proxyUrl":"Proxy URL","proxyUrlPlaceholder":"socks5://user:pass@host:port","proxyHint":"Supports: socks5://user:pass@host:port or host:port:user:pass","testing":"Testing...","emailStrategy":"Email Strategy","emailStrategyDesc":"Choose how to generate emails for registration","save":"Save","createProfile":"Create Profile","enterYourEmail":"Enter your email","detected":"Detected","chooseStrategy":"Choose strategy","recommended":"Recommended","otherOptions":"Other options","useAppPassword":"Use App Password","imapConnection":"IMAP Connection","optional":"optional","checkConnection":"Check connection","strategySingleName":"Single Email","strategySingleDesc":"Uses your IMAP email directly. Only 1 account per email.","strategySingleExample":"your@gmail.com → your@gmail.com","strategyPlusAliasName":"Plus Alias","strategyPlusAliasDesc":"Adds +random to your email. All emails arrive to same inbox. Works with Gmail, Outlook, Yandex.","strategyPlusAliasExample":"your@gmail.com → your+kiro5x7@gmail.com","strategyCatchAllName":"Catch-All Domain","strategyCatchAllDesc":"Generates random emails on your domain. Requires catch-all configured on mail server.","strategyCatchAllExample":"JohnSmith4521@yourdomain.com","strategyCatchAllHint":"Enter domain with catch-all configured. All emails to any@domain will arrive to your IMAP inbox.","strategyCatchAllDomain":"Registration Domain","strategyPoolName":"Email Pool","strategyPoolDesc":"Use a list of your email addresses. Each email is used once in order.","strategyPoolHint":"Add email addresses to use in order. Each email = 1 account. Format: email:password","strategyPoolAdd":"email:password or email...","strategyPoolFromFile":"From file","strategyPoolPaste":"Paste","poolEmpty":"Add at least one email to pool","emailsAdded":"{count} emails added","example":"Example","unlimitedAccounts":"Unlimited accounts","allEmailsOneInbox":"All emails in one inbox","noOwnDomain":"No own domain needed","notAllProvidersSupport":"Not all providers support","uniqueEmails":"Unique email addresses","needOwnDomain":"Need own domain","needCatchAllSetup":"Need catch-all setup","easyToSetup":"Easy to setup","worksEverywhere":"Works everywhere","oneAccountPerEmail":"Only 1 account per email","worksWithAnyProvider":"Works with any provider","controlOverList":"Control over list","needManyEmails":"Need many emails","requiresDomain":"Requires own domain","providerNoAlias":"does not support aliases","activeProfile":"Active Profile","change":"Change","noProfileConfigured":"No profile configured","configure":"Configure","emailProfiles":"Email Profiles","noProfiles":"No profiles configured","addProfile":"Add Profile","success":"Success","failed":"Failed","strategySingleShort":"One account per email","strategyPlusAliasShort":"user+random@domain","strategyCatchAllShort":"Any email on domain","strategyPoolShort":"Your email list","dangerZone":"Danger Zone","resetMachineId":"Reset Machine ID","resetMachineIdDesc":"Generate new telemetry IDs for Kiro. Use if account is banned.","resetMachineIdTip":"Reset machineId, sqmId, devDeviceId and serviceMachineId","reset":"Reset","restartAfterReset":"Restart Kiro after reset","resetMachineIdTitle":"Reset Machine ID","resetMachineIdConfirm":"This will reset Kiro telemetry IDs. You need to restart Kiro after. Continue?","llmSettings":"LLM API","llmBaseUrl":"Base URL","llmPort":"Port","llmApiKey":"API Key (optional)","llmApiKeyDesc":"If set, all requests must include this key in the Authorization header.","llmModel":"Default Model","llmModelDesc":"Default chat model to use for completions.","llmServer":"LLM Server","llmServerDesc":"Start or stop the local OpenAI-compatible server.","startServer":"Start Server","stopServer":"Stop Server","restartServer":"Restart Server","starting":"Starting...","stopping":"Stopping...","restarting":"Restarting...","serverStatus":"Server Status","serverStatusRunning":"Running","serverStatusStopped":"Stopped","serverStatusError":"Error","proxySettings":"Proxy","proxySettingsDesc":"Route browser traffic through proxy server","proxyUrlHint":"Formats: host:port:user:pass, socks5://user:pass@host:port","delete":"Delete","checkUpdates":"Check Updates","newVersion":"New version!","download":"Download","edit":"Edit","unnamed":"Unnamed","customDomain":"Custom Domain","emailPasswordHint":"Use your email password","ssoImport":"SSO Import","ssoHint":"1. Open view.awsapps.com/start\n2. DevTools → Application → Cookies\n3. Copy x-amz-sso_authn","pasteCookie":"Paste cookie...","providerGmail":"Gmail","providerYandex":"Yandex","providerMailru":"Mail.ru","providerOutlook":"Outlook","providerCustom":"Custom Domain","gmailPasswordHint":"Use App Password from Google Account settings","yandexPasswordHint":"Use app password from Yandex settings","mailruPasswordHint":"Create app password in Mail.ru settings","outlookPasswordHint":"Use your Microsoft account password","accountDeleted":"Account deleted","badAccountsDeleted":"Bad accounts deleted","resettingMachineId":"Resetting Machine ID...","patchingKiro":"Patching Kiro...","removingPatch":"Removing patch...","profileCreated":"Profile created","profileUpdated":"Profile updated","profileDeleted":"Profile deleted","tokenCopied":"Token copied to clipboard","logsCopied":"Logs copied to clipboard","fillAllFields":"Please fill all IMAP fields","clipboardError":"Failed to read clipboard","deleteProfileConfirm":"Delete this profile?","deleteBadAccountsConfirm":"Delete all expired/exhausted accounts?","deleteBannedAccountsConfirm":"Delete all banned accounts?","bannedAccountsDeleted":"Banned accounts deleted","emailsImported":"Imported {count} emails","exportAccounts":"Export Accounts","exportAccountsDesc":"Export accounts with tokens for transfer","importAccounts":"Import Accounts","importAccountsDesc":"Import accounts from export file","exportSelected":"Export Selected","exportAll":"Export All","selectMode":"Select","selected":"selected","selectAll":"Select All","deselectAll":"Deselect All","refreshSelected":"Refresh Tokens","deleteSelected":"Delete Selected","deleteSelectedConfirm":"Delete {count} selected accounts?","selectedAccountsDeleted":"{count} accounts deleted","refreshingTokens":"Refreshing tokens...","checkHealth":"Check Health","checkingHealth":"Checking accounts health...","ready":"Ready","clickToConfigure":"Click to configure","noAccountsFound":"No accounts found","clearSearch":"Clear search","emptyAccountsTitle":"No accounts yet","emptyAccountsDesc":"Create your first account to get started with Kiro","emptySearchTitle":"No results found","emptySearchDesc":"Try a different search term","emptyBannedTitle":"No banned accounts","emptyBannedDesc":"All your accounts are in good standing","automation":"Automation","interface":"Interface","autoSwitchThreshold":"Switch threshold","autoSwitchThresholdDesc":"Switch when remaining < this value","pause":"Pause","statistics":"Statistics","weeklyUsage":"Weekly Usage","avgPerAccount":"Avg per account","accountHealth":"Account Health","registrationStrategy":"Registration Strategy","registrationStrategyDesc":"Choose how to register accounts (affects ban risk)","strategyWebView":"WebView (Recommended)","strategyWebViewDesc":"Opens real browser, you enter credentials manually. Minimal ban risk.","strategyWebViewBanRisk":"Low ban risk (<10%)","strategyWebViewFeature1":"Real browser fingerprint","strategyWebViewFeature2":"Human-like behavior","strategyAutomated":"Automated (Legacy)","strategyAutomatedDesc":"Automated browser with DrissionPage. Works for some users.","strategyAutomatedBanRisk":"Medium-High ban risk (40-90%)","strategyAutomatedFeature1":"Fully automatic","strategyAutomatedFeature2":"High detection risk","deferQuotaCheck":"Defer quota check","deferQuotaCheckDesc":"Do NOT check quota immediately after registration (reduces ban risk)","manualInputRequired":"Manual input required","lowBanRisk":"Low risk","mediumBanRisk":"Medium risk","highBanRisk":"High risk","banRiskLabel":"Ban risk","auto":"Auto","manual":"Manual","oauthProvider":"OAuth Provider","oauthProviderDesc":"Choose provider for WebView registration","oauthProviderGoogle":"Google","oauthProviderGithub":"GitHub","oauthProviderAsk":"Ask every time","oauthProviderAskDesc":"Show provider selection dialog","selectOAuthProvider":"Select OAuth Provider","selectOAuthProviderDesc":"Choose how to sign in to Kiro","batchRegistration":"Batch Registration","scheduledRegistration":"Scheduled Registration","scheduledRegistrationDesc":"Auto-register accounts at intervals","loginTemplate":"Login Template","loginTemplatePlaceholder":"Account_{N}","loginTemplateHint":"Use {N} for number (001, 002...)","startNumber":"Start #","scheduleInterval":"Interval","scheduleIntervalManual":"Manual","scheduleIntervalMinutes":"min","maxAccounts":"Limit","maxAccountsHint":"Max accounts to register","startSchedule":"Start","stopSchedule":"Stop","scheduleActive":"Active","scheduleNextIn":"Next in","complete":"Complete","preview":"Preview","nextNumber":"Next #","interval":"Interval","minutes":"min","hour":"hour","hours":"hours","startScheduled":"Start","resetProgress":"Reset counter to start","noTimer":"No timer","noTimerHint":"Click Start for each registration","custom":"Custom","upcoming":"Upcoming","startFrom":"Start #","enableFirst":"Enable toggle first","registerOne":"Register","accountsToRegister":"Accounts to register","intervalBetween":"Interval between registrations","noDelay":"No delay","noDelayHint":"Registrations will run one after another","withDelayHint":"Safer for avoiding rate limits","accountNames":"Account names","randomNames":"Random names","randomNamesDesc":"Realistic first + last names","customPrefix":"Custom prefix","customPrefixDesc":"Your prefix + number","enterPrefix":"Enter prefix (e.g. MyAcc)","exampleNames":"Example names","previousProgress":"Previous progress","startBatchReg":"Start Registration","stopRegistration":"Stop Registration","completed":"Completed","batchRemaining":"Remaining","nextIn":"Next in","current":"Current","proxyAddress":"Proxy Address","proxyAddressPlaceholder":"http://user:pass@host:port","testProxy":"Test Proxy","testingProxy":"Testing...","proxyWorking":"Proxy is working","proxyNotWorking":"Proxy is not working","proxyNotConfigured":"Proxy not configured","proxyResponseTime":"Response time","proxyIpAddress":"IP Address","useProxyForRegistration":"Use proxy for registration","useProxyForRegistrationDesc":"Route registration traffic through proxy","proxyTestSuccess":"Proxy test successful","proxyTestFailed":"Proxy test failed","addAccount":"Add Account","deleteDoubleClick":"Click twice to delete","addFirstAccount":"Add your first account to get started"};
// vscode API provided by WebSocket adapter above
let pendingAction = null;
let focusedAccountIndex = -1;
// Client-side HTML escaping (uses DOM API, different from server-side helpers.ts version)
function escapeHtmlClient(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Registration step definitions for progress indicators
const REG_STEPS = [
{ id: 'setup', icon: '⚙️', name: 'Setup' },
{ id: 'email', icon: '📧', name: 'Email' },
{ id: 'browser', icon: '🌐', name: 'Browser' },
{ id: 'signup', icon: '📝', name: 'Sign Up' },
{ id: 'verify', icon: '✉️', name: 'Verify' },
{ id: 'auth', icon: '🔐', name: 'Auth' },
{ id: 'token', icon: '🎫', name: 'Token' },
{ id: 'done', icon: '✅', name: 'Done' }
];
function renderStepIndicatorsJS(currentStep, totalSteps, error) {
const steps = REG_STEPS.slice(0, totalSteps);
const stepsHtml = steps.map((step, i) => {
const stepNum = i + 1;
let status = 'pending';
if (stepNum < currentStep) status = 'done';
else if (stepNum === currentStep) status = error ? 'error' : 'active';
return '<div class="step-indicator ' + status + '" title="' + step.name + '">' +
'<span class="step-icon">' + step.icon + '</span>' +
'<span class="step-dot"></span>' +
'</div>';
}).join('<div class="step-line"></div>');
return '<div class="step-indicators">' + stepsHtml + '</div>';
}
const STATE = {"filter":"all","sort":"email","compact":false,"settingsOpen":false,"searchQuery":"","scrollPosition":0,"activeTab":"accounts","disableToasts":false};
let ACCOUNT_INDEX = {};
function getState() { return STATE; }
function setState(partial) {
Object.assign(STATE, partial);
try { localStorage.setItem('kiro-state', JSON.stringify(STATE)); } catch {}
}
function loadState() {
try {
const saved = localStorage.getItem('kiro-state');
if (saved) Object.assign(STATE, JSON.parse(saved));
} catch {}
}
loadState();
// === Tab Navigation ===
let currentTab = 'accounts';
function switchTab(tabId) {
if (currentTab === tabId) return;
const previousTab = currentTab;
currentTab = tabId;
setState({ activeTab: tabId });
// Update tab buttons
document.querySelectorAll('.tab-item').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
// Animate tab content transition
const previousContent = document.getElementById('tab-' + previousTab);
const newContent = document.getElementById('tab-' + tabId);
// Fade out previous
if (previousContent) {
previousContent.classList.add('tab-fade-out');
previousContent.classList.remove('active');
}
// Fade in new
if (newContent) {
newContent.classList.add('tab-fade-in');
newContent.classList.add('active');
// Scroll to top
newContent.scrollTop = 0;
// Remove animation classes after transition
setTimeout(() => {
if (previousContent) previousContent.classList.remove('tab-fade-out');
newContent.classList.remove('tab-fade-in');
}, 200);
}
// FAB visibility - only show on accounts tab
const fab = document.getElementById('fabContainer');
if (fab) {
fab.style.display = tabId === 'accounts' ? '' : 'none';
}
// Reset keyboard navigation when switching tabs
focusedAccountIndex = -1;
document.querySelectorAll('.account.keyboard-focus').forEach(acc => {
acc.classList.remove('keyboard-focus');
});
// Load data for specific tabs
if (tabId === 'profiles') {
vscode.postMessage({ command: 'loadProfiles' });
vscode.postMessage({ command: 'getActiveProfile' });
} else if (tabId === 'settings') {
// Settings tab now includes Stats and LLM
vscode.postMessage({ command: 'getPatchStatus' });
vscode.postMessage({ command: 'getActiveProfile' });
getLLMSettings();
vscode.postMessage({ command: 'getLLMServerStatus' });
}
}
// === UI Actions ===
function openSettings() {
switchTab('settings');
// Load active profile when opening settings
vscode.postMessage({ command: 'getActiveProfile' });
// Load patch status
vscode.postMessage({ command: 'getPatchStatus' });
}
function closeSettings() {
document.getElementById('settingsOverlay')?.classList.remove('visible');
}
// Render active profile in settings
function renderActiveProfile(profile) {
const container = document.getElementById('activeProfileContent');
if (!container) return;
const strategyLabels = {
single: { icon: '📧', name: T.strategySingleName, desc: T.strategySingleShort },
plus_alias: { icon: '➕', name: T.strategyPlusAliasName, desc: T.strategyPlusAliasShort },
catch_all: { icon: '🌐', name: T.strategyCatchAllName, desc: T.strategyCatchAllShort },
pool: { icon: '📋', name: T.strategyPoolName, desc: T.strategyPoolShort }
};
if (!profile) {
container.innerHTML = `
<div class="active-profile-empty">
<span class="empty-icon">📧</span>
<span class="empty-text">${T.noProfileConfigured}</span>
<button class="btn btn-primary btn-sm" onclick="openProfilesPanel()">${T.configure}</button>
</div>
`;
return;
}
const strategy = strategyLabels[profile.strategy?.type] || strategyLabels.single;
const stats = profile.stats || { registered: 0, failed: 0 };
container.innerHTML = `
<div class="active-profile-info">
<div class="active-profile-avatar">${strategy.icon}</div>
<div class="active-profile-details">
<div class="active-profile-name">${profile.name || T.unnamed}</div>
<div class="active-profile-email">${profile.imap?.user || ''}</div>
<div class="active-profile-strategy">
<span class="strategy-name">${strategy.name}</span>
<span class="strategy-desc">· ${strategy.desc}</span>
</div>
</div>
</div>
<div class="active-profile-stats">
<div class="active-profile-stat">
<span class="active-profile-stat-value success">${stats.registered}</span>
<span class="active-profile-stat-label">${T.success}</span>
</div>
<div class="active-profile-stat">
<span class="active-profile-stat-value danger">${stats.failed}</span>
<span class="active-profile-stat-label">${T.failed}</span>
</div>
</div>
`;
}
function toggleAutoSwitch(enabled) {
vscode.postMessage({ command: 'toggleAutoSwitch', enabled });
// Show/hide threshold row
const thresholdRow = document.getElementById('autoSwitchThresholdRow');
if (thresholdRow) {
thresholdRow.style.display = enabled ? '' : 'none';
}
}
function toggleSetting(key, value) {
vscode.postMessage({ command: 'updateSetting', key, value });
}
function updateSetting(key, value) {
vscode.postMessage({ command: 'updateSetting', key, value });
}
function selectRegistrationStrategy(strategy) {
// Update switch buttons on main page
document.querySelectorAll('.strategy-sw-btn').forEach((btn, idx) => {
const isAuto = strategy === 'automated';
if (idx === 0) {
btn.classList.toggle('active', isAuto);
} else {
btn.classList.toggle('active', !isAuto);
}
});
// Update hint icon
const hint = document.querySelector('.strategy-hint');
if (hint) {
hint.className = 'strategy-hint ' + (strategy === 'automated' ? 'high' : 'low');
hint.textContent = strategy === 'automated' ? '⚠' : '✓';
}
// Update strategy cards in settings (if visible)
document.querySelectorAll('.strategy-option').forEach(option => {
const radio = option.querySelector('input[name="strategy"]');
if (radio) {
radio.checked = radio.value === strategy;
option.classList.toggle('selected', radio.value === strategy);
}
});
// Show/hide defer quota check option in settings
const deferOption = document.getElementById('deferQuotaCheckOption');
if (deferOption) {
deferOption.style.display = strategy === 'automated' ? '' : 'none';
}
// Show/hide OAuth provider option in settings
const oauthOption = document.getElementById('oauthProviderOption');
if (oauthOption) {
oauthOption.style.display = strategy === 'webview' ? '' : 'none';
}
// Save setting
vscode.postMessage({ command: 'updateSetting', key: 'strategy', value: strategy });
}
function toggleSpoofing(enabled) {
vscode.postMessage({ command: 'updateSetting', key: 'spoofing', value: enabled });
// Toggle details visibility
const details = document.getElementById('spoofDetails');
if (details) {
details.classList.toggle('hidden', !enabled);
}
}
function changeLanguage(lang) {
vscode.postMessage({ command: 'setLanguage', language: lang });
}
function toggleSettingsCard(header, event) {
if (event) {
// Prevent toggle if clicking on a button or input inside the header
const target = event.target;
if (target.tagName === 'BUTTON' || target.tagName === 'INPUT' || target.closest('.toggle')) {
return;
}
event.stopPropagation();
}
const card = header.closest('.settings-card');
if (card) {
card.classList.toggle('collapsed');
}
}
function checkUpdates() {
vscode.postMessage({ command: 'checkForUpdates' });
}
function exportAllAccounts() {
vscode.postMessage({ command: 'exportAccounts' });
}
function importAccounts() {
vscode.postMessage({ command: 'importAccounts' });
}
function confirmResetMachineId() {
pendingAction = { type: 'resetMachineId' };
document.getElementById('dialogTitle').textContent = T.resetMachineIdTitle;
document.getElementById('dialogText').textContent = T.resetMachineIdConfirm;
// Change button to Confirm (not Delete)
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.confirm;
btn.className = 'btn btn-warning';
document.getElementById('dialogOverlay').classList.add('visible');
}
function resetMachineId() {
vscode.postMessage({ command: 'resetMachineId' });
}
// === Kiro Patching ===
function confirmPatchKiro() {
pendingAction = { type: 'patchKiro' };
document.getElementById('dialogTitle').textContent = T.patchKiroTitle;
document.getElementById('dialogText').textContent = T.patchKiroConfirm;
// Change button to Apply (not Delete)
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.apply;
btn.className = 'btn btn-primary';
document.getElementById('dialogOverlay').classList.add('visible');
}
function confirmUnpatchKiro() {
pendingAction = { type: 'unpatchKiro' };
document.getElementById('dialogTitle').textContent = T.removePatchTitle;
document.getElementById('dialogText').textContent = T.removePatchConfirm;
// Change button to Confirm (not Delete)
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.confirm;
btn.className = 'btn btn-warning';
document.getElementById('dialogOverlay').classList.add('visible');
}
function patchKiro(force = false) {
vscode.postMessage({ command: 'patchKiro', force });
}
function unpatchKiro() {
vscode.postMessage({ command: 'unpatchKiro' });
}
function generateNewMachineId() {
vscode.postMessage({ command: 'generateMachineId' });
}
function getPatchStatus() {
vscode.postMessage({ command: 'getPatchStatus' });
}
function openVsCodeSettings() {
vscode.postMessage({ command: 'openVsCodeSettings' });
}
// === LLM Server ===
function getLLMSettings() {
vscode.postMessage({ command: 'getLLMSettings' });
}
function saveLLMSettings() {
const settings = {
baseUrl: document.getElementById('llmBaseUrl')?.value || '',
port: document.getElementById('llmPort')?.value || '8421',
apiKey: document.getElementById('llmApiKey')?.value || '',
model: document.getElementById('llmModel')?.value || 'claude-sonnet-4-20250514',
};
vscode.postMessage({ command: 'saveLLMSettings', settings });
showToast('LLM settings saved', 'success');
}
function startLLMServer() {
const btn = document.getElementById('llmStartBtn');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ ' + T.starting;
}
updateLLMServerStatus({ status: 'Starting...' });
vscode.postMessage({ command: 'startLLMServer' });
}
function stopLLMServer() {
const btn = document.getElementById('llmStopBtn');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ ' + T.stopping;
}
updateLLMServerStatus({ status: 'Stopping...' });
vscode.postMessage({ command: 'stopLLMServer' });
}
function restartLLMServer() {
const btn = document.getElementById('llmRestartBtn');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ ' + T.restarting;
}
updateLLMServerStatus({ status: 'Restarting...' });
vscode.postMessage({ command: 'restartLLMServer' });
}
function updateLLMServerStatus(status) {
const statusEl = document.getElementById('llmServerStatus');
const startBtn = document.getElementById('llmStartBtn');
const stopBtn = document.getElementById('llmStopBtn');
const restartBtn = document.getElementById('llmRestartBtn');
if (statusEl) {
const statusText = status.status || (status.running ? 'Running' : 'Stopped');
statusEl.textContent = statusText;
statusEl.className = 'patch-status ' + statusText.toLowerCase().replace('...', '');
}
// Re-enable buttons and update text
const isRunning = status.running || status.status === 'Running';
const isPending = status.status?.includes('...');
if (startBtn) {
startBtn.disabled = isPending || isRunning;
startBtn.textContent = T.startServer;
}
if (stopBtn) {
stopBtn.disabled = isPending || !isRunning;
stopBtn.textContent = T.stopServer;
}
if (restartBtn) {
restartBtn.disabled = isPending;
restartBtn.textContent = T.restartServer;
}
// Load models when server is running
if (isRunning && !isPending) {
vscode.postMessage({ command: 'getLLMModels' });
}
}
function updateLLMModels(models) {
const select = document.getElementById('llmModel');
if (!select || !models || !models.length) return;
const currentValue = select.value;
select.innerHTML = '';
models.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
// Show model id with credit info from description
const credit = model.description?.match(/\([^)]+\)/)?.[0] || '';
option.textContent = model.id + (credit ? ' ' + credit : '');
select.appendChild(option);
});
// Restore selection if exists
if (currentValue) {
select.value = currentValue;
}
}
function updateLLMSettings(settings) {
const baseUrlEl = document.getElementById('llmBaseUrl');
const portEl = document.getElementById('llmPort');
const apiKeyEl = document.getElementById('llmApiKey');
const modelEl = document.getElementById('llmModel');
if (baseUrlEl) baseUrlEl.value = settings.baseUrl || 'http://127.0.0.1';
if (portEl) portEl.value = settings.port || '8421';
if (apiKeyEl) apiKeyEl.value = settings.apiKey || '';
if (modelEl) modelEl.value = settings.model || 'claude-sonnet-4-20250514';
}
function startAutoReg() {
const countInput = document.getElementById('regCountInput');
const count = countInput ? parseInt(countInput.value, 10) : 1;
vscode.postMessage({ command: 'startAutoReg', count: count > 1 ? count : undefined });
}
function startQuickAutoReg() {
// Quick autoreg - always 1 account with current strategy
vscode.postMessage({ command: 'startAutoReg', count: 1 });
}
function stopAutoReg() {
vscode.postMessage({ command: 'stopAutoReg' });
}
function togglePauseAutoReg() {
vscode.postMessage({ command: 'togglePauseAutoReg' });
}
function refresh() {
vscode.postMessage({ command: 'refresh' });
}
function refreshUsage() {
vscode.postMessage({ command: 'refreshUsage' });
}
function switchAccount(filename) {
// Add switching state to show loading feedback
const accountEl = document.querySelector('.account[data-filename="' + filename + '"]');
if (accountEl) {
accountEl.classList.add('switching');
}
vscode.postMessage({ command: 'switchAccount', email: filename });
}
function copyToken(filename) {
vscode.postMessage({ command: 'copyToken', email: filename });
}
function refreshToken(filename) {
vscode.postMessage({ command: 'refreshToken', email: filename });
}
function openUpdateUrl(url) {
vscode.postMessage({ command: 'openUrl', url: url });
}
document.addEventListener('click', (e) => {
const target = e.target;
const link = target?.closest?.('a[href]');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('#')) return;
e.preventDefault();
e.stopPropagation();
openUpdateUrl(href);
}, true);
// === SSO Modal ===
function openSsoModal() {
document.getElementById('ssoModal')?.classList.add('visible');
}
function closeSsoModal() {
document.getElementById('ssoModal')?.classList.remove('visible');
const input = document.getElementById('ssoTokenInput');
if (input) input.value = '';
}
function importSsoToken() {
const input = document.getElementById('ssoTokenInput');
const token = input?.value?.trim();
if (token) {
vscode.postMessage({ command: 'importSsoToken', token });
closeSsoModal();
}
}
// === Account Secrets Modal ===
function openAccountSecrets(filename) {
const modal = document.getElementById('accountSecretsModal');
const acc = ACCOUNT_INDEX[filename];
if (!modal || !acc) return;
const tokenData = acc.tokenData || {};
const email = tokenData.email || '';
const refreshToken = tokenData.refreshToken || '';
const clientId = tokenData.clientId || '';
const clientSecret = tokenData.clientSecret || '';
const emailEl = document.getElementById('accountSecretEmail');
const refreshEl = document.getElementById('accountSecretRefreshToken');
const clientIdEl = document.getElementById('accountSecretClientId');
const clientSecretEl = document.getElementById('accountSecretClientSecret');
if (emailEl) emailEl.value = email;
if (refreshEl) refreshEl.value = refreshToken;
if (clientIdEl) clientIdEl.value = clientId;
if (clientSecretEl) clientSecretEl.value = clientSecret;
modal.classList.add('visible');
}
function closeAccountSecrets() {
const modal = document.getElementById('accountSecretsModal');
modal?.classList.remove('visible');
}
function copySecretField(id) {
const el = document.getElementById(id);
const value = el?.value || '';
if (!value) return;
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(value).then(() => {
showToast('Copied', 'success');
}).catch(() => {
showToast('Copy failed', 'error');
});
} else {
try {
el.focus();
el.select();
document.execCommand('copy');
showToast('Copied', 'success');
} catch {
showToast('Copy failed', 'error');
}
}
}
// === Console Drawer ===
function toggleLogs() {
const drawer = document.getElementById('logsDrawer');
drawer?.classList.toggle('open');
}
// === Batch Registration Toggle ===
function toggleBatchReg(event) {
if (event) {
const target = event.target;
if (target.tagName === 'BUTTON' || target.tagName === 'INPUT') {
return;
}
event.stopPropagation();
}
const card = document.getElementById('scheduledRegCard');
if (card) {
card.classList.toggle('collapsed');
const toggle = card.querySelector('.batch-reg-toggle');
if (toggle) {
toggle.textContent = card.classList.contains('collapsed') ? '▼' : '▲';
}
}
}
function clearConsole() {
const content = document.getElementById('logsContent');
if (content) content.innerHTML = '';
updateLogsCount();
vscode.postMessage({ command: 'clearConsole' });
}
function copyLogs() {
const content = document.getElementById('logsContent');
if (content) {
const logs = Array.from(content.querySelectorAll('.console-line'))
.map(el => el.textContent?.trim())
.filter(Boolean)
.join('\n');
vscode.postMessage({ command: 'copyLogs', logs });
}
}
function filterConsole(type) {
const content = document.getElementById('logsContent');
const filters = document.querySelectorAll('.console-filter');
// Update active filter button
filters.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-filter') === type);
});
// Filter log lines
if (content) {
const lines = content.querySelectorAll('.console-line');
lines.forEach(line => {
if (type === 'all') {
line.classList.remove('hidden');
} else {
const lineType = line.getAttribute('data-type');
line.classList.toggle('hidden', lineType !== type);
}
});
}
}
function updateLogsCount() {
const content = document.getElementById('logsContent');
const countEl = document.getElementById('logsCount');
if (content && countEl) {
const count = content.querySelectorAll('.console-line:not(.hidden)').length;
const hasErrors = content.querySelector('.console-line.error') !== null;
const hasWarnings = content.querySelector('.console-line.warning') !== null;
countEl.textContent = count.toString();
countEl.classList.remove('error', 'warning');
if (hasErrors) countEl.classList.add('error');
else if (hasWarnings) countEl.classList.add('warning');
}
}
// Track if user has scrolled up
let isScrolledToBottom = true;
let hasNewMessages = false;
function handleConsoleScroll() {
const content = document.getElementById('logsContent');
if (!content) return;
const threshold = 50; // pixels from bottom
const scrolledToBottom = content.scrollHeight - content.scrollTop - content.clientHeight < threshold;
isScrolledToBottom = scrolledToBottom;
// Update scroll button visibility
const scrollBtn = document.getElementById('scrollToBottomBtn');
if (scrollBtn) {
scrollBtn.classList.toggle('hidden', scrolledToBottom);
}
// Hide new messages bar if scrolled to bottom
if (scrolledToBottom) {
hasNewMessages = false;
const newMsgBar = document.getElementById('newMessagesBar');
newMsgBar?.classList.remove('visible');
const newIndicator = document.getElementById('newLogsIndicator');
newIndicator?.classList.remove('visible');
}
}
function scrollConsoleToBottom() {
const content = document.getElementById('logsContent');
if (content) {
content.scrollTo({
top: content.scrollHeight,
behavior: 'smooth'
});
isScrolledToBottom = true;
hasNewMessages = false;
// Hide indicators
const newMsgBar = document.getElementById('newMessagesBar');
newMsgBar?.classList.remove('visible');
const newIndicator = document.getElementById('newLogsIndicator');
newIndicator?.classList.remove('visible');
const scrollBtn = document.getElementById('scrollToBottomBtn');
scrollBtn?.classList.add('hidden');
}
}
function showNewMessagesIndicator() {
if (!isScrolledToBottom) {
hasNewMessages = true;
const newMsgBar = document.getElementById('newMessagesBar');
newMsgBar?.classList.add('visible');
const newIndicator = document.getElementById('newLogsIndicator');
newIndicator?.classList.add('visible');
}
}
// Syntax highlighting helper
function highlightLogMessage(message) {
let highlighted = escapeHtmlClient(message);
// Highlight paths
highlighted = highlighted.replace(
/([A-Za-z]:)?[\/][\w\-\.\/\\]+/g,
'<span class="hl-path">$&</span>'
);
// Highlight URLs
highlighted = highlighted.replace(
/(https?:\/\/[^\s<]+)/g,
'<span class="hl-url">$1</span>'
);
// Highlight numbers
highlighted = highlighted.replace(
/\b(\d+(?:\.\d+)?)\b/g,
'<span class="hl-number">$1</span>'
);
// Highlight quoted strings
highlighted = highlighted.replace(
/(&quot;[^&]*&quot;|&#39;[^&]*&#39;|"[^"]*"|'[^']*')/g,
'<span class="hl-string">$1</span>'
);
// Highlight keywords
highlighted = highlighted.replace(
/\b(SUCCESS|FAIL|ERROR|WARN|OK|DONE|START|STOP|TRUE|FALSE|NULL|NONE)\b/gi,
'<span class="hl-keyword">$1</span>'
);
// Highlight email addresses
highlighted = highlighted.replace(
/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
'<span class="hl-email">$1</span>'
);
return highlighted;
}
function appendLogLine(log) {
const content = document.getElementById('logsContent');
if (!content) return;
// Determine log type
let type = 'info';
let icon = '›';
if (log.includes('ERROR') || log.includes('FAIL') || log.includes('✗') || log.includes('❌')) {
type = 'error';
icon = '✗';
} else if (log.includes('SUCCESS') || log.includes('✓') || log.includes('✅') || log.includes('[OK]')) {
type = 'success';
icon = '✓';
} else if (log.includes('WARN') || log.includes('⚠') || log.includes('⛔')) {
type = 'warning';
icon = '!';
}
// Extract time if present
const timeMatch = log.match(/^\[?(\d{1,2}:\d{2}:\d{2})\s*[AP]?M?\]?/i);
const time = timeMatch ? timeMatch[1] : '';
const message = timeMatch ? log.slice(timeMatch[0].length).trim() : log;
// Check for duplicate message (grouping)
const lastLine = content.lastElementChild;
if (lastLine) {
const lastMsg = lastLine.querySelector('.console-msg')?.textContent;
const lastType = lastLine.getAttribute('data-type');
if (lastMsg === message && lastType === type) {
// Increment count badge
let countBadge = lastLine.querySelector('.console-count');
if (countBadge) {
const count = parseInt(countBadge.textContent || '1') + 1;
countBadge.textContent = count.toString();
} else {
countBadge = document.createElement('span');
countBadge.className = 'console-count';
countBadge.textContent = '2';
lastLine.appendChild(countBadge);
}
// Update time
const timeEl = lastLine.querySelector('.console-time');
if (timeEl && time) {
timeEl.textContent = time;
}
return;
}
}
// Create line element with animation
const line = document.createElement('div');
line.className = `console-line ${type}`;
line.setAttribute('data-type', type);
line.innerHTML = `
<span class="console-icon">${icon}</span>
${time ? `<span class="console-time">${time}</span>` : ''}
<span class="console-msg">${highlightLogMessage(message)}</span>
`;
content.appendChild(line);
// Auto-scroll only if already at bottom
if (isScrolledToBottom) {
content.scrollTop = content.scrollHeight;
} else {
showNewMessagesIndicator();
}
// Check current filter
const activeFilter = document.querySelector('.console-filter.active');
const currentFilter = activeFilter?.getAttribute('data-filter') || 'all';
if (currentFilter !== 'all' && currentFilter !== type) {
line.classList.add('hidden');
}
// Keep max 200 lines
while (content.children.length > 200 && content.firstChild) {
content.removeChild(content.firstChild);
}
updateLogsCount();
updateConsoleStatus();
// Auto-open on errors
if (type === 'error') {
document.getElementById('logsDrawer')?.classList.add('open');
}
}
function updateConsoleStatus() {
const content = document.getElementById('logsContent');
const drawer = document.getElementById('logsDrawer');
if (!content || !drawer) return;
const hasErrors = content.querySelector('.console-line.error') !== null;
const hasWarnings = content.querySelector('.console-line.warning') !== null;
drawer.classList.remove('has-errors', 'has-warnings');
if (hasErrors) {
drawer.classList.add('has-errors');
} else if (hasWarnings) {
drawer.classList.add('has-warnings');
}
// Update indicator
const indicator = drawer.querySelector('.console-icon-indicator');
if (indicator) {
indicator.classList.remove('has-errors', 'has-warnings');
if (hasErrors) indicator.classList.add('has-errors');
else if (hasWarnings) indicator.classList.add('has-warnings');
}
}
// === Delete with Double-Click (no modal) ===
let pendingDeleteFilename = null;
let pendingDeleteTimeout = null;
function confirmDelete(filename) {
const btn = event?.target?.closest('.account-btn.danger');
// If same file clicked again within timeout - delete!
if (pendingDeleteFilename === filename && btn) {
clearTimeout(pendingDeleteTimeout);
pendingDeleteFilename = null;
btn.classList.remove('confirm-delete');
vscode.postMessage({ command: 'deleteAccount', email: filename });
showToast(T.accountDeleted, 'success');
return;
}
// First click - highlight button and wait for second click
// Reset any previous pending delete
if (pendingDeleteTimeout) {
clearTimeout(pendingDeleteTimeout);
document.querySelectorAll('.account-btn.danger.confirm-delete').forEach(b => {
b.classList.remove('confirm-delete');
});
}
pendingDeleteFilename = filename;
if (btn) {
btn.classList.add('confirm-delete');
}
// Reset after 3 seconds
pendingDeleteTimeout = setTimeout(() => {
pendingDeleteFilename = null;
if (btn) btn.classList.remove('confirm-delete');
}, 3000);
}
function confirmDeleteExhausted() {
pendingAction = { type: 'deleteExhausted' };
document.getElementById('dialogTitle').textContent = T.deleteTitle;
document.getElementById('dialogText').textContent = T.deleteBadAccountsConfirm;
// Reset button to Delete style
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.delete;
btn.className = 'btn btn-danger';
document.getElementById('dialogOverlay').classList.add('visible');
}
function confirmDeleteBanned() {
pendingAction = { type: 'deleteBanned' };
document.getElementById('dialogTitle').textContent = T.deleteTitle;
document.getElementById('dialogText').textContent = T.deleteBannedAccountsConfirm || 'Delete all banned accounts?';
// Reset button to Delete style
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.delete;
btn.className = 'btn btn-danger';
document.getElementById('dialogOverlay').classList.add('visible');
}
function refreshAllExpired() {
vscode.postMessage({ command: 'refreshAllExpired' });
}
function checkAllAccountsHealth() {
vscode.postMessage({ command: 'checkAllAccountsHealth' });
showToast(T.checkingHealth || 'Checking accounts health...', 'success');
}
function closeDialog() {
document.getElementById('dialogOverlay').classList.remove('visible');
pendingAction = null;
}
function dialogAction() {
if (pendingAction?.type === 'delete') {
vscode.postMessage({ command: 'deleteAccount', email: pendingAction.filename });
showToast(T.accountDeleted, 'success');
} else if (pendingAction?.type === 'deleteExhausted') {
vscode.postMessage({ command: 'deleteExhaustedAccounts' });
showToast(T.badAccountsDeleted, 'success');
} else if (pendingAction?.type === 'deleteBanned') {
vscode.postMessage({ command: 'deleteBannedAccounts' });
showToast(T.bannedAccountsDeleted || 'Banned accounts deleted', 'success');
} else if (pendingAction?.type === 'deleteSelected') {
vscode.postMessage({ command: 'deleteSelectedAccounts', filenames: pendingAction.filenames });
showToast((T.selectedAccountsDeleted || '{count} accounts deleted').replace('{count}', pendingAction.filenames.length), 'success');
selectionMode = false;
selectedAccounts.clear();
} else if (pendingAction?.type === 'deleteProfile') {
vscode.postMessage({ command: 'deleteProfile', profileId: pendingAction.profileId });
showToast(T.profileDeleted || 'Profile deleted', 'success');
} else if (pendingAction?.type === 'resetMachineId') {
vscode.postMessage({ command: 'resetMachineId' });
showToast(T.resettingMachineId, 'success');
} else if (pendingAction?.type === 'patchKiro') {
vscode.postMessage({ command: 'patchKiro' });
showToast(T.patchingKiro, 'success');
} else if (pendingAction?.type === 'unpatchKiro') {
vscode.postMessage({ command: 'unpatchKiro' });
showToast(T.removingPatch, 'success');
}
closeDialog();
}
// === Search & Filters ===
let searchQuery = '';
let tokenFilter = 'all'; // all, fresh, partial, trial, empty
function searchAccounts(query) {
searchQuery = query.toLowerCase().trim();
applyFilters();
}
function clearSearch() {
const input = document.getElementById('searchInput');
if (input) input.value = '';
searchQuery = '';
applyFilters();
}
function filterByTokens(filter) {
tokenFilter = filter;
// Update select value (for dropdown)
const select = document.getElementById('tokenFilterSelect');
if (select) select.value = filter;
applyFilters();
}
function getAccountTokens(acc) {
// Extract remaining tokens from usage text (format: "123/500")
const usageText = acc.querySelector('.account-meta span:first-child')?.textContent || '';
const match = usageText.match(/(\d+)\/(\d+)/);
if (!match) return -1;
return parseInt(match[1], 10);
}
function applyFilters() {
let visibleCount = 0;
document.querySelectorAll('.account').forEach(acc => {
const email = (acc.querySelector('.account-email')?.textContent || '').toLowerCase();
const searchMatch = !searchQuery || email.includes(searchQuery);
// Token filter
let tokenMatch = true;
if (tokenFilter !== 'all') {
const tokens = getAccountTokens(acc);
switch (tokenFilter) {
case 'fresh':
tokenMatch = tokens === 500;
break;
case 'partial':
tokenMatch = tokens > 0 && tokens < 500 && tokens !== 50;
break;
case 'trial':
tokenMatch = tokens === 50;
break;
case 'empty':
tokenMatch = tokens === 0;
break;
}
}
const match = searchMatch && tokenMatch;
acc.style.display = match ? '' : 'none';
if (match) visibleCount++;
});
// Show/hide empty search state
const emptySearch = document.getElementById('emptySearchState');
if (emptySearch) {
emptySearch.style.display = ((searchQuery || tokenFilter !== 'all') && visibleCount === 0) ? 'block' : 'none';
}
// Hide group headers if all accounts in group are hidden
document.querySelectorAll('.list-group').forEach(group => {
let nextEl = group.nextElementSibling;
let hasVisible = false;
while (nextEl && !nextEl.classList.contains('list-group')) {
if (nextEl.classList.contains('account') && nextEl.style.display !== 'none') {
hasVisible = true;
break;
}
nextEl = nextEl.nextElementSibling;
}
group.style.display = hasVisible ? '' : 'none';
});
}
// === Toast ===
function showToast(message, type = 'success') {
if (getState()?.disableToasts) return;
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = 'toast ' + type;
const icons = { success: '✓', error: '✗', warning: '⚠️', info: 'ℹ' };
toast.innerHTML = '<span class="toast-icon">' + (icons[type] || '•') + '</span><span class="toast-message">' + message + '</span>';
// Add entering animation class
toast.classList.add('toast-entering');
container.appendChild(toast);
// Trigger reflow for animation
toast.offsetHeight;
// Remove entering class to start animation
requestAnimationFrame(() => {
toast.classList.remove('toast-entering');
toast.classList.add('toast-visible');
});
// Auto-dismiss after 3 seconds
setTimeout(() => {
toast.classList.add('toast-removing');
toast.classList.remove('toast-visible');
setTimeout(() => toast.remove(), 300);
}, 3000);
// Allow manual dismiss on click
toast.addEventListener('click', () => {
toast.classList.add('toast-removing');
toast.classList.remove('toast-visible');
setTimeout(() => toast.remove(), 300);
});
}
// === Skeleton Loading ===
function renderSkeletonCards(count = 3) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="account skeleton">
<div class="account-avatar skeleton-pulse"></div>
<div class="account-info">
<div class="skeleton-line skeleton-pulse" style="width: 70%"></div>
<div class="skeleton-line skeleton-pulse" style="width: 40%"></div>
</div>
</div>
`;
}
return html;
}
function showSkeletonLoading() {
const list = document.getElementById('accountList');
if (list) {
list.innerHTML = renderSkeletonCards(3);
}
}
function hideSkeletonLoading() {
const skeletons = document.querySelectorAll('.account.skeleton');
skeletons.forEach(skeleton => {
skeleton.classList.add('skeleton-fade-out');
setTimeout(() => skeleton.remove(), 200);
});
}
// === Message Handler ===
window.addEventListener('message', (event) => {
const msg = event.data;
switch (msg.type) {
case 'appendLog':
appendLogLine(msg.log);
break;
case 'updateStatus':
updateStatus(msg.status);
break;
case 'updateAccounts':
{
const list = document.getElementById('accountList');
if (list && typeof msg.html === 'string') {
list.innerHTML = msg.html;
}
// Remove switching state from all accounts after update
document.querySelectorAll('.account.switching').forEach(el => {
el.classList.remove('switching');
});
// Update header badge counts if provided
const badge = document.querySelector('.header-badge');
if (badge && typeof msg.validCount === 'number' && typeof msg.totalCount === 'number') {
badge.textContent = msg.validCount + '/' + msg.totalCount;
}
// Optional: update tab badges by triggering a light state write (tab bar is static HTML)
// If full rerender is needed for badges, keep it out of hot path.
}
break;
case 'accountsLoaded':
// Standalone WebSocket sends accountsLoaded with accounts array
if (msg.accounts && Array.isArray(msg.accounts)) {
ACCOUNT_INDEX = {};
msg.accounts.forEach(function(acc) {
ACCOUNT_INDEX[acc.filename] = acc;
});
const list = document.getElementById('accountList');
if (list) {
if (msg.accounts.length === 0) {
list.innerHTML = '<div class="empty-state"><div class="empty-icon">👤</div><h3 class="empty-title">' + (T.noAccounts || 'No accounts yet') + '</h3><p class="empty-desc">' + (T.addFirstAccount || 'Add your first account') + '</p><button class="btn btn-primary" onclick="startQuickAutoReg()">➕ ' + (T.addAccount || 'Add Account') + '</button></div>';
} else {
// Render accounts from JSON data
list.innerHTML = msg.accounts.map(function(acc) {
const isActive = acc.isActive ? 'active' : '';
const statusClass = acc.isExpired ? 'expired' : (acc.needsRefresh ? 'warning' : 'valid');
const name = acc.tokenData?.accountName || acc.filename || 'Unknown';
const email = acc.tokenData?.email || '';
const safeFilename = (acc.filename || '').replace(/'/g, "\\'");
const usage = acc.usage || {};
const usageText = usage.currentUsage >= 0 ? usage.currentUsage + '/' + (usage.usageLimit || 500) : '—';
const usagePercent = usage.percentageUsed || 0;
return '<div class="account ' + isActive + ' ' + statusClass + '" data-filename="' + acc.filename + '" onclick="switchAccount(\'' + safeFilename + '\')">' +
'<div class="account-info">' +
'<div class="account-name">' + name + '</div>' +
'<div class="account-email">' + email + '</div>' +
'</div>' +
'<div class="account-usage">' +
'<div class="usage-bar"><div class="usage-fill" style="width:' + usagePercent + '%"></div></div>' +
'<div class="usage-text">' + usageText + '</div>' +
'</div>' +
'<div class="account-actions">' +
'<button class="btn btn-sm" onclick="event.stopPropagation(); refreshToken(\'' + safeFilename + '\')">🔄</button>' +
'<button class="btn btn-sm" onclick="event.stopPropagation(); openAccountSecrets(\'' + safeFilename + '\')">🔑</button>' +
'<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); confirmDelete(\'' + safeFilename + '\')">🗑️</button>' +
'</div>' +
'</div>';
}).join('');
}
}
// Update header badge
const badge = document.querySelector('.header-badge');
if (badge) {
const valid = msg.accounts.filter(function(a) { return !a.isExpired; }).length;
badge.textContent = valid + '/' + msg.accounts.length;
}
}
break;
case 'updateUsage':
// Incremental usage update - refresh hero section
if (msg.usage) updateHeroUsage(msg.usage);
else clearHeroUsage();
break;
case 'toast':
showToast(msg.message, msg.toastType || 'success');
break;
case 'profilesLoaded':
renderProfilesList(msg.profiles, msg.activeProfileId);
break;
case 'activeProfileLoaded':
renderActiveProfile(msg.profile);
break;
case 'profileLoaded':
populateProfileEditor(msg.profile);
break;
case 'providerDetected':
applyProviderHint(msg.hint, msg.recommendedStrategy);
break;
case 'patchStatus':
updatePatchStatus(msg);
break;
case 'llmServerStatus':
updateLLMServerStatus(msg.status);
break;
case 'llmSettings':
updateLLMSettings(msg.settings);
break;
case 'llmModels':
updateLLMModels(msg.models);
break;
case 'scheduledRegState':
updateScheduledRegState(msg.state);
break;
case 'imapTestResult':
updateImapTestResult(msg);
break;
}
});
function updateHeroUsage(usage) {
const hero = document.querySelector('.hero');
if (!hero || hero.classList.contains('progress')) return;
const current = typeof usage.currentUsage === 'number' ? usage.currentUsage : 0;
const limit = typeof usage.usageLimit === 'number' ? usage.usageLimit : 500;
const percent = typeof usage.percentageUsed === 'number' ? usage.percentageUsed : 0;
const remaining = limit - current;
const usageClass = percent >= 95 ? 'high' : percent >= 80 ? 'medium' : 'low';
const isLow = remaining < 50;
const isCritical = remaining < 10;
hero.classList.toggle('warning', isLow && !isCritical);
hero.classList.toggle('critical', isCritical);
const valueEl = hero.querySelector('.hero-value');
if (valueEl) {
valueEl.textContent = remaining.toLocaleString();
valueEl.className = 'hero-value ' + usageClass;
}
const fillEl = hero.querySelector('.hero-progress-fill');
if (fillEl) {
fillEl.style.width = Math.min(percent, 100) + '%';
fillEl.className = 'hero-progress-fill ' + usageClass;
}
const footerStats = hero.querySelectorAll('.hero-footer .hero-stat');
if (footerStats && footerStats.length > 0) {
footerStats[0].textContent = current.toLocaleString() + '/' + limit + ' ' + (T.used || 'used');
}
}
function clearHeroUsage() {
const hero = document.querySelector('.hero');
if (!hero || hero.classList.contains('progress')) return;
hero.classList.remove('warning', 'critical');
const valueEl = hero.querySelector('.hero-value');
if (valueEl) {
valueEl.textContent = '?';
valueEl.className = 'hero-value';
}
const fillEl = hero.querySelector('.hero-progress-fill');
if (fillEl) {
fillEl.style.width = '0%';
fillEl.className = 'hero-progress-fill';
}
const footerStats = hero.querySelectorAll('.hero-footer .hero-stat');
if (footerStats && footerStats.length > 0) {
footerStats[0].textContent = '—';
}
}
function updateImapTestResult(result) {
const btn = document.getElementById('testConnectionBtn');
if (!btn) return;
if (result.status === 'testing') {
btn.disabled = true;
btn.innerHTML = '⏳ ' + (T.testing || 'Testing...');
btn.className = 'btn btn-secondary';
} else if (result.status === 'success') {
btn.disabled = false;
btn.innerHTML = '✅ ' + (T.connected || 'Connected!');
btn.className = 'btn btn-success';
showToast(result.message, 'success');
// Reset after 3 seconds
setTimeout(() => {
btn.innerHTML = '🔌 ' + T.testConnection;
btn.className = 'btn btn-secondary';
}, 3000);
} else {
btn.disabled = false;
btn.innerHTML = '❌ ' + (T.failed || 'Failed');
btn.className = 'btn btn-danger';
showToast(result.message, 'error');
// Reset after 3 seconds
setTimeout(() => {
btn.innerHTML = '🔌 ' + T.testConnection;
btn.className = 'btn btn-secondary';
}, 3000);
}
}
function updatePatchStatus(status) {
if (!status) return;
const patchBtn = document.getElementById('patchKiroBtn');
const updateBtn = document.getElementById('updatePatchBtn');
const unpatchBtn = document.getElementById('unpatchKiroBtn');
const generateBtn = document.getElementById('generateIdBtn');
const statusEl = document.getElementById('patchStatusText');
const versionInfoEl = document.getElementById('patchVersionInfo');
const machineIdEl = document.getElementById('currentMachineId');
const indicator = document.getElementById('patchIndicator');
// Update settings panel status
if (statusEl) {
if (status.error) {
statusEl.textContent = status.error;
statusEl.className = 'patch-status error';
} else if (status.isPatched) {
if (status.needsUpdate) {
statusEl.textContent = T.patchStatusOutdated + ' ⚠️';
statusEl.className = 'patch-status warning';
} else {
statusEl.textContent = T.patchStatusActive + ' ✓';
statusEl.className = 'patch-status success';
}
} else {
statusEl.textContent = T.patchStatusNotPatched;
statusEl.className = 'patch-status warning';
}
}
// Update version info
if (versionInfoEl) {
if (status.isPatched && status.patchVersion) {
let versionText = T.patchVersion + ' v' + status.patchVersion;
if (status.latestPatchVersion && status.patchVersion !== status.latestPatchVersion) {
versionText += ' → v' + status.latestPatchVersion;
}
if (status.kiroVersion) {
versionText += ' | ' + T.kiroVersion + ' v' + status.kiroVersion;
}
versionInfoEl.textContent = versionText;
versionInfoEl.style.display = '';
} else if (status.kiroVersion) {
versionInfoEl.textContent = T.kiroVersion + ' v' + status.kiroVersion;
versionInfoEl.style.display = '';
} else {
versionInfoEl.style.display = 'none';
}
}
// Update machine ID preview
if (machineIdEl && status.currentMachineId) {
machineIdEl.textContent = 'ID: ' + status.currentMachineId.substring(0, 16) + '...';
machineIdEl.title = status.currentMachineId;
}
// Update header indicator
if (indicator) {
indicator.className = 'patch-indicator visible';
if (status.error) {
indicator.classList.add('error');
indicator.title = status.error;
} else if (status.isPatched) {
if (status.needsUpdate) {
indicator.classList.add('needs-update');
indicator.title = T.patchUpdateAvailable + ': v' + status.patchVersion + ' → v' + status.latestPatchVersion;
} else {
indicator.classList.add('patched');
indicator.title = T.patchStatusActive + ' (v' + status.patchVersion + ')';
}
} else if (status.currentMachineId) {
// Has custom ID but not patched - needs attention
indicator.classList.add('not-patched');
indicator.title = T.patchStatusNotPatched;
indicator.onclick = openSettings;
} else {
// No custom ID, no patch - hide indicator
indicator.className = 'patch-indicator';
}
}
// Update buttons visibility
if (status.isPatched) {
if (patchBtn) patchBtn.style.display = 'none';
if (updateBtn) updateBtn.style.display = status.needsUpdate ? '' : 'none';
if (unpatchBtn) unpatchBtn.style.display = '';
} else {
if (patchBtn) patchBtn.style.display = '';
if (updateBtn) updateBtn.style.display = 'none';
if (unpatchBtn) unpatchBtn.style.display = 'none';
}
}
function updateStatus(status) {
const btn = document.querySelector('.btn-primary');
const hero = document.querySelector('.hero');
const fab = document.getElementById('fabContainer');
const autoregControls = document.querySelector('.autoreg-controls');
if (!status) {
// Registration finished
if (btn) {
btn.disabled = false;
btn.innerHTML = '⚡ ' + T.autoReg;
}
// Update FAB state - restore primary button
if (fab) {
fab.classList.remove('running');
fab.innerHTML = `
<button class="fab fab-primary pulse" onclick="startAutoReg()" title="${T.autoReg}">
<span class="fab-icon"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9 1L4 9h4l-1 6 5-8H8l1-6z"/></svg></span>
</button>
`;
}
// Restore autoreg controls to idle state
if (autoregControls) {
autoregControls.classList.remove('running');
autoregControls.innerHTML = `
<div class="form-group">
<label for="regCountInput">${T.autoRegCountLabel || 'Count'}</label>
<input type="number" id="regCountInput" class="form-control" value="1" min="1" max="100" placeholder="${T.autoRegCountPlaceholder || '1'}">
</div>
<button class="btn btn-primary pulse" onclick="startAutoReg()" title="${T.autoRegTip || T.autoReg}">
▶️ <span class="btn-text">${T.autoReg}</span>
</button>
`;
}
// Refresh to show new account
vscode.postMessage({ command: 'refresh' });
return;
}
// Show running state
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> ' + T.running;
}
// Update autoreg controls to running state with stop/pause buttons
if (autoregControls) {
autoregControls.classList.add('running');
autoregControls.innerHTML = `
<button class="btn btn-danger" onclick="stopAutoReg()" title="${T.stop || 'Stop'}">
⏹ <span class="btn-text">${T.stop || 'Stop'}</span>
</button>
<button class="btn btn-secondary" onclick="togglePauseAutoReg()" title="${T.pause || 'Pause'}">
⏸ <span class="btn-text">${T.pause || 'Pause'}</span>
</button>
`;
}
// Update FAB to running state
if (fab) {
fab.classList.add('running');
// Update FAB content to show stop/pause buttons
fab.innerHTML = `
<button class="fab fab-stop" onclick="stopAutoReg()" title="${T.stop || 'Stop'}">
<span class="fab-icon">⏹</span>
</button>
<button class="fab fab-pause" onclick="togglePauseAutoReg()" title="${T.pause || 'Pause'}">
<span class="fab-icon">⏸</span>
</button>
<div class="fab-status">
<span class="spinner"></span>
<span class="fab-status-text">${T.running}</span>
</div>
`;
}
// Update hero with progress (incremental update, no full refresh)
try {
const progress = JSON.parse(status);
if (progress && hero) {
const percent = Math.round((progress.step / progress.totalSteps) * 100);
const hasError = (progress.detail || '').toLowerCase().includes('error') ||
(progress.detail || '').toLowerCase().includes('fail');
// Only update hero content, preserve console drawer state
hero.className = 'hero progress';
hero.innerHTML = `
<div class="hero-header">
<span class="hero-email">${progress.stepName || ''}</span>
<span class="hero-step">${progress.step}/${progress.totalSteps}</span>
</div>
${renderStepIndicatorsJS(progress.step, progress.totalSteps, hasError)}
<div class="hero-progress">
<div class="hero-progress-fill ${hasError ? 'high' : 'low'}" style="width: ${percent}%"></div>
</div>
<div class="hero-stats">
<span class="hero-usage ${hasError ? 'text-danger' : ''}">${progress.detail || ''}</span>
<span class="hero-percent">${percent}%</span>
</div>
`;
}
} catch {}
}
// === Keyboard Shortcuts ===
document.addEventListener('keydown', (e) => {
const target = e.target;
const isInputFocused = target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement;
// Escape - close modals/drawer
if (e.key === 'Escape') {
// Close in order of priority
const logsDrawer = document.getElementById('logsDrawer');
if (logsDrawer?.classList.contains('open')) {
logsDrawer.classList.remove('open');
return;
}
if (document.getElementById('ssoModal')?.classList.contains('visible')) {
closeSsoModal();
return;
}
if (document.getElementById('profileEditor')?.classList.contains('visible')) {
closeProfileEditor();
return;
}
if (document.getElementById('dialogOverlay')?.classList.contains('visible')) {
closeDialog();
return;
}
// Clear search if has value
const searchInput = document.getElementById('searchInput');
if (searchInput && searchInput.value) {
clearSearch();
searchInput.blur();
return;
}
}
// Cmd/Ctrl+K - focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}
// Cmd/Ctrl+F - also focus search (legacy)
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}
// Cmd/Ctrl+R - refresh
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
e.preventDefault();
refresh();
showToast(T.refreshing || 'Refreshing...', 'success');
}
// Arrow keys - navigate accounts (only when not in input)
if (!isInputFocused && currentTab === 'accounts') {
const accounts = Array.from(document.querySelectorAll('.account:not([style*="display: none"])'));
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
focusedAccountIndex = Math.min(focusedAccountIndex + 1, accounts.length - 1);
focusAccount(accounts, focusedAccountIndex);
}
if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
focusedAccountIndex = Math.max(focusedAccountIndex - 1, 0);
focusAccount(accounts, focusedAccountIndex);
}
// Enter - switch to focused account
if (e.key === 'Enter' && focusedAccountIndex >= 0 && focusedAccountIndex < accounts.length) {
e.preventDefault();
const account = accounts[focusedAccountIndex];
const filename = account.getAttribute('data-filename');
if (filename) switchAccount(filename);
}
// Delete/Backspace - delete focused account
if ((e.key === 'Delete' || e.key === 'Backspace') && focusedAccountIndex >= 0 && focusedAccountIndex < accounts.length) {
e.preventDefault();
const account = accounts[focusedAccountIndex];
const filename = account.getAttribute('data-filename');
if (filename) confirmDelete(filename);
}
}
// Tab navigation with numbers (1-4)
if (!isInputFocused && e.key >= '1' && e.key <= '4') {
const tabs = ['accounts', 'profiles', 'llm', 'settings'];
const tabIndex = parseInt(e.key) - 1;
if (tabs[tabIndex]) {
switchTab(tabs[tabIndex]);
}
}
});
function focusAccount(accounts, index) {
// Remove focus from all
accounts.forEach(acc => acc.classList.remove('keyboard-focus'));
// Add focus to current
if (index >= 0 && index < accounts.length) {
const account = accounts[index];
account.classList.add('keyboard-focus');
account.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Reset focus index when clicking
document.addEventListener('click', () => {
focusedAccountIndex = -1;
document.querySelectorAll('.account.keyboard-focus').forEach(acc => {
acc.classList.remove('keyboard-focus');
});
});
// === IMAP Profiles ===
let currentPoolEmails = [];
let editingProfileId = null;
function openProfilesPanel() {
// For tab navigation - just switch to profiles tab
switchTab('profiles');
}
function closeProfilesPanel() {
// Legacy - switch back to accounts
switchTab('accounts');
}
function createProfile() {
editingProfileId = null;
currentPoolEmails = [];
// Show editor form, hide list (for inline mode)
const listContainer = document.getElementById('profilesListContainer');
const editorForm = document.getElementById('profileEditorForm');
if (listContainer) listContainer.style.display = 'none';
if (editorForm) editorForm.style.display = 'block';
// Legacy overlay mode
document.getElementById('profileEditor')?.classList.add('visible');
// Reset form
const nameEl = document.getElementById('profileName');
const userEl = document.getElementById('imapUser');
const serverEl = document.getElementById('imapServer');
const portEl = document.getElementById('imapPort');
const passwordEl = document.getElementById('imapPassword');
const proxyCheckbox = document.getElementById('proxyEnabled');
const proxyUrlsEl = document.getElementById('proxyUrls');
const proxyFields = document.getElementById('proxyFields');
if (nameEl) nameEl.value = '';
if (userEl) userEl.value = '';
if (serverEl) serverEl.value = '';
if (portEl) portEl.value = '993';
if (passwordEl) passwordEl.value = '';
if (proxyCheckbox) proxyCheckbox.checked = false;
if (proxyUrlsEl) proxyUrlsEl.value = '';
if (proxyFields) proxyFields.style.display = 'none';
selectStrategy('single');
// Update title
const title = document.querySelector('.editor-title');
if (title) title.textContent = T.newProfile || 'New Profile';
}
function editProfile(profileId) {
editingProfileId = profileId;
vscode.postMessage({ command: 'getProfile', profileId });
// Show editor form, hide list (for inline mode)
const listContainer = document.getElementById('profilesListContainer');
const editorForm = document.getElementById('profileEditorForm');
if (listContainer) listContainer.style.display = 'none';
if (editorForm) editorForm.style.display = 'block';
}
function closeProfileEditor() {
// Hide editor form, show list (for inline mode)
const listContainer = document.getElementById('profilesListContainer');
const editorForm = document.getElementById('profileEditorForm');
if (listContainer) listContainer.style.display = 'block';
if (editorForm) editorForm.style.display = 'none';
// Legacy overlay mode
document.getElementById('profileEditor')?.classList.remove('visible');
editingProfileId = null;
}
function toggleProxyFields() {
const checkbox = document.getElementById('proxyEnabled');
const fields = document.getElementById('proxyFields');
if (checkbox && fields) {
fields.style.display = checkbox.checked ? 'block' : 'none';
}
}
function selectProfile(profileId) {
vscode.postMessage({ command: 'setActiveProfile', profileId });
}
function deleteProfile(profileId) {
// Use custom dialog instead of confirm() which doesn't work in webview
pendingAction = { type: 'deleteProfile', profileId };
document.getElementById('dialogTitle').textContent = T.deleteTitle || 'Delete';
document.getElementById('dialogText').textContent = T.deleteProfileConfirm;
// Reset button to Delete style
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.delete;
btn.className = 'btn btn-danger';
document.getElementById('dialogOverlay').classList.add('visible');
}
function selectStrategy(strategy) {
document.querySelectorAll('.strategy-option').forEach(el => {
el.classList.toggle('selected', el.dataset.strategy === strategy);
});
const catchAllConfig = document.getElementById('catchAllConfig');
const poolConfig = document.getElementById('poolConfig');
const imapEmailGroup = document.getElementById('imapEmailGroup');
const imapPasswordGroup = document.getElementById('imapPasswordGroup');
const testConnectionBtn = document.getElementById('testConnectionBtn');
if (catchAllConfig) catchAllConfig.style.display = strategy === 'catch_all' ? 'block' : 'none';
if (poolConfig) poolConfig.style.display = strategy === 'pool' ? 'block' : 'none';
// For Pool strategy - hide email/password fields (they come from pool list)
const isPool = strategy === 'pool';
if (imapEmailGroup) imapEmailGroup.style.display = isPool ? 'none' : 'block';
if (imapPasswordGroup) imapPasswordGroup.style.display = isPool ? 'none' : 'block';
if (testConnectionBtn) testConnectionBtn.style.display = isPool ? 'none' : 'block';
}
function onEmailInput(email) {
vscode.postMessage({ command: 'detectProvider', email });
}
function testImapConnection() {
const server = document.getElementById('imapServer')?.value?.trim();
const user = document.getElementById('imapUser')?.value?.trim();
const password = document.getElementById('imapPassword')?.value;
const port = document.getElementById('imapPort')?.value || '993';
// Validate fields before testing
if (!server || !user || !password) {
showToast(T.fillAllFields || 'Please fill all IMAP fields', 'error');
return;
}
vscode.postMessage({ command: 'testImap', server, user, password, port: parseInt(port) });
}
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
if (input) input.type = input.type === 'password' ? 'text' : 'password';
}
function addEmailToPool() {
const input = document.getElementById('newPoolEmail');
const value = input?.value?.trim();
if (!value || !value.includes('@')) return;
// Parse email:password format
let email, password;
if (value.includes(':') && value.indexOf(':') > value.indexOf('@')) {
const colonPos = value.lastIndexOf(':');
const atPos = value.indexOf('@');
if (colonPos > atPos) {
email = value.substring(0, colonPos);
password = value.substring(colonPos + 1);
} else {
email = value;
}
} else {
email = value;
}
const existing = currentPoolEmails.find(e => (e.email || e).toLowerCase() === email.toLowerCase());
if (!existing) {
currentPoolEmails.push(password ? { email, password } : { email });
renderPoolList();
}
if (input) input.value = '';
}
function removeEmailFromPool(index) {
currentPoolEmails.splice(index, 1);
renderPoolList();
}
function renderPoolList() {
const list = document.getElementById('poolList');
if (!list) return;
list.innerHTML = currentPoolEmails.map((item, i) => {
const email = item.email || item;
const hasPassword = item.password ? ' 🔑' : '';
const status = item.status || 'pending';
const statusIcon = status === 'used' ? '✅' : status === 'failed' ? '❌' : '⬜';
const statusClass = status === 'used' ? 'used' : status === 'failed' ? 'failed' : 'pending';
const errorTip = item.error ? ' title="' + item.error + '"' : '';
return '<div class="pool-item ' + statusClass + '" data-index="' + i + '"' + errorTip + '>' +
'<span class="pool-status">' + statusIcon + '</span>' +
'<span class="pool-email">' + email + hasPassword + '</span>' +
(status === 'pending' ? '<button class="pool-remove" onclick="removeEmailFromPool(' + i + ')">✕</button>' : '') +
'</div>';
}).join('');
// Show pool stats
const used = currentPoolEmails.filter(e => e.status === 'used').length;
const failed = currentPoolEmails.filter(e => e.status === 'failed').length;
const pending = currentPoolEmails.length - used - failed;
const statsEl = document.getElementById('poolStats');
if (statsEl) {
statsEl.innerHTML = '<span class="pool-stat success">' + used + ' ✅</span> ' +
'<span class="pool-stat danger">' + failed + ' ❌</span> ' +
'<span class="pool-stat">' + pending + ' ⬜</span>';
}
}
function importEmailsFromFile() {
vscode.postMessage({ command: 'importEmailsFromFile' });
}
function parseAndAddEmails(text) {
// Support formats: email, email:password, one per line or separated by newlines
const lines = text.split(new RegExp('[\\r\\n]+')).filter(e => e.includes('@'));
let added = 0;
lines.forEach(line => {
const trimmed = line.trim();
// Parse email:password format
let email, password;
if (trimmed.includes(':') && trimmed.indexOf(':') > trimmed.indexOf('@')) {
const colonPos = trimmed.lastIndexOf(':');
const atPos = trimmed.indexOf('@');
if (colonPos > atPos) {
email = trimmed.substring(0, colonPos);
password = trimmed.substring(colonPos + 1);
} else {
email = trimmed;
}
} else {
email = trimmed;
}
const existing = currentPoolEmails.find(e => e.email?.toLowerCase() === email.toLowerCase() || e === email.toLowerCase());
if (!existing) {
currentPoolEmails.push(password ? { email, password } : { email });
added++;
}
});
return added;
}
function pasteEmails() {
navigator.clipboard.readText().then(text => {
const added = parseAndAddEmails(text);
renderPoolList();
if (added > 0) {
showToast((T.emailsAdded || '{count} emails added').replace('{count}', added), 'success');
}
}).catch(() => {
showToast(T.clipboardError, 'error');
});
}
function handlePoolPaste(event) {
const text = (event.clipboardData || window.clipboardData)?.getData('text');
if (!text) return;
// Check if pasted text contains multiple lines or email:password format
const hasMultipleLines = text.includes('\n') || text.includes('\r');
const hasEmailPassword = text.includes('@') && text.includes(':') && text.indexOf(':') > text.indexOf('@');
if (hasMultipleLines || hasEmailPassword) {
event.preventDefault();
const added = parseAndAddEmails(text);
renderPoolList();
if (added > 0) {
showToast((T.emailsAdded || '{count} emails added').replace('{count}', added), 'success');
}
// Clear input
event.target.value = '';
}
// If single email without password - let default paste behavior work
}
function saveProfile() {
const name = document.getElementById('profileName')?.value?.trim() || T.unnamed;
const server = document.getElementById('imapServer')?.value?.trim();
const user = document.getElementById('imapUser')?.value?.trim();
const password = document.getElementById('imapPassword')?.value;
const port = parseInt(document.getElementById('imapPort')?.value) || 993;
// Proxy settings
const proxyEnabled = document.getElementById('proxyEnabled')?.checked || false;
const proxyUrlsText = document.getElementById('proxyUrls')?.value?.trim() || '';
const proxyUrls = proxyUrlsText.split('\n').map(u => u.trim()).filter(u => u.length > 0);
const proxy = proxyEnabled && proxyUrls.length > 0 ? { enabled: true, urls: proxyUrls, currentIndex: 0 } : undefined;
const selectedStrategy = document.querySelector('.strategy-option.selected');
const strategyType = selectedStrategy?.dataset?.strategy || 'single';
const strategy = { type: strategyType };
if (strategyType === 'catch_all') {
strategy.domain = document.getElementById('catchAllDomain')?.value?.trim();
} else if (strategyType === 'pool') {
// Support both old format (string) and new format (object with email/password)
strategy.emails = currentPoolEmails.map(item => {
const email = item.email || item;
const pwd = item.password;
return { email, password: pwd, status: 'pending' };
});
}
// For Pool strategy - email/password come from pool list, not from IMAP fields
const isPool = strategyType === 'pool';
if (isPool) {
// Validate pool has entries
if (!currentPoolEmails || currentPoolEmails.length === 0) {
showToast(T.poolEmpty || 'Add at least one email to pool', 'error');
return;
}
if (!server) {
showToast(T.fillAllFields, 'error');
return;
}
// Use first pool email as fallback IMAP credentials
const firstEntry = currentPoolEmails[0];
const firstEmail = firstEntry.email || firstEntry;
const firstPassword = firstEntry.password || '';
vscode.postMessage({
command: editingProfileId ? 'updateProfile' : 'createProfile',
profile: {
id: editingProfileId,
name,
imap: { server, user: firstEmail, password: firstPassword, port },
strategy,
proxy
}
});
} else {
// For other strategies - require all IMAP fields
if (!server || !user || !password) {
showToast(T.fillAllFields, 'error');
return;
}
vscode.postMessage({
command: editingProfileId ? 'updateProfile' : 'createProfile',
profile: {
id: editingProfileId,
name,
imap: { server, user, password, port },
strategy,
proxy
}
});
}
closeProfileEditor();
}
// === Profile Message Handlers ===
function renderProfilesList(profiles, activeId) {
const container = document.getElementById('profilesContent');
if (!container) return;
if (!profiles || profiles.length === 0) {
container.innerHTML = `
<div class="profiles-empty">
<div class="empty-icon">📧</div>
<div class="empty-text">${T.noProfiles}</div>
<button class="btn btn-primary" onclick="createProfile()">+ ${T.addProfile}</button>
</div>
`;
return;
}
const strategyLabels = {
single: T.strategySingleName,
plus_alias: T.strategyPlusAliasName,
catch_all: T.strategyCatchAllName,
pool: T.strategyPoolName
};
const strategyIcons = {
single: '📧',
plus_alias: '➕',
catch_all: '🌐',
pool: '📋'
};
let html = '<div class="profiles-list">';
profiles.forEach(profile => {
const isActive = profile.id === activeId;
const strategyType = profile.strategy?.type || 'single';
const stats = profile.stats || { registered: 0, failed: 0 };
html += `
<div class="profile-card ${isActive ? 'active' : ''}" data-id="${profile.id}">
<div class="profile-card-header">
<div class="profile-card-radio" onclick="selectProfile('${profile.id}')">
<span class="radio-dot ${isActive ? 'checked' : ''}"></span>
</div>
<div class="profile-card-info" onclick="editProfile('${profile.id}')">
<div class="profile-card-name">${profile.name || T.unnamed}</div>
<div class="profile-card-email">${profile.imap?.user || ''}</div>
</div>
<div class="profile-card-actions">
<button class="icon-btn" onclick="editProfile('${profile.id}')" title="${T.edit}">✏️</button>
<button class="icon-btn danger" onclick="deleteProfile('${profile.id}')" title="${T.delete}">🗑</button>
</div>
</div>
<div class="profile-card-meta">
<span class="profile-strategy">${strategyIcons[strategyType]} ${strategyLabels[strategyType]}</span>
<span class="profile-stats">✓ ${stats.registered} / ✗ ${stats.failed}</span>
</div>
</div>
`;
});
html += '</div>';
html += `<button class="btn btn-primary profiles-add-btn" onclick="createProfile()">+ ${T.addProfile}</button>`;
container.innerHTML = html;
}
function populateProfileEditor(profile) {
if (!profile) return;
editingProfileId = profile.id;
document.getElementById('profileName').value = profile.name || '';
document.getElementById('imapUser').value = profile.imap?.user || '';
document.getElementById('imapServer').value = profile.imap?.server || '';
document.getElementById('imapPort').value = profile.imap?.port || 993;
document.getElementById('imapPassword').value = profile.imap?.password || '';
// Proxy settings
const proxyCheckbox = document.getElementById('proxyEnabled');
const proxyUrlsInput = document.getElementById('proxyUrls');
const proxyFields = document.getElementById('proxyFields');
const proxyStats = document.getElementById('proxyStats');
if (proxyCheckbox && proxyUrlsInput) {
const hasProxy = profile.proxy?.enabled || false;
proxyCheckbox.checked = hasProxy;
proxyUrlsInput.value = (profile.proxy?.urls || []).join('\n');
if (proxyFields) {
proxyFields.style.display = hasProxy ? 'block' : 'none';
}
if (proxyStats && profile.proxy?.urls) {
proxyStats.textContent = profile.proxy.urls.length + ' proxies';
}
}
const strategyType = profile.strategy?.type || 'single';
selectStrategy(strategyType);
if (strategyType === 'catch_all' && profile.strategy?.domain) {
document.getElementById('catchAllDomain').value = profile.strategy.domain;
}
if (strategyType === 'pool' && profile.strategy?.emails) {
// Keep full email objects with status
currentPoolEmails = profile.strategy.emails.map(e => ({
email: e.email,
password: e.password,
status: e.status || 'pending',
error: e.error
}));
renderPoolList();
}
document.getElementById('profileEditor')?.classList.add('visible');
// Update editor title
const title = document.querySelector('.editor-title');
if (title) {
title.textContent = T.editProfile;
}
}
function applyProviderHint(hint, recommendedStrategy) {
if (!hint) return;
const serverInput = document.getElementById('imapServer');
const portInput = document.getElementById('imapPort');
const hintEl = document.getElementById('providerHint');
if (serverInput && !serverInput.value) {
serverInput.value = hint.imapServer || '';
}
if (portInput && !portInput.value) {
portInput.value = hint.imapPort || 993;
}
if (hintEl) {
const aliasSupport = hint.supportsAlias
? '✓ ' + T.strategyPlusAliasName
: '✗ ' + hint.name + ' ' + T.providerNoAlias;
hintEl.innerHTML = `<span class="provider-name">${hint.name}</span> · ${aliasSupport}`;
hintEl.style.display = 'block';
}
// Auto-select recommended strategy
if (recommendedStrategy) {
selectStrategy(recommendedStrategy);
}
}
function addImportedEmails(emails) {
if (!emails || !Array.isArray(emails)) return;
emails.forEach(email => {
const e = email.trim().toLowerCase();
if (e && e.includes('@') && !currentPoolEmails.includes(e)) {
currentPoolEmails.push(email.trim());
}
});
renderPoolList();
showToast(T.emailsImported.replace('{count}', emails.length), 'success');
}
// === Selection Mode (Bulk Actions) ===
let selectionMode = false;
let selectedAccounts = new Set();
function toggleSelectionMode() {
selectionMode = !selectionMode;
selectedAccounts.clear();
// Toggle bulk actions bar visibility
const bar = document.getElementById('bulkActionsBar');
const selectBtn = document.getElementById('selectModeBtn');
if (bar) bar.classList.toggle('hidden', !selectionMode);
if (selectBtn) selectBtn.classList.toggle('active', selectionMode);
// Toggle checkbox visibility - add/remove checkboxes dynamically
document.querySelectorAll('.account').forEach(card => {
let checkbox = card.querySelector('.account-checkbox');
if (selectionMode) {
if (!checkbox) {
const filename = card.dataset.filename;
checkbox = document.createElement('label');
checkbox.className = 'account-checkbox';
checkbox.onclick = (e) => e.stopPropagation();
checkbox.innerHTML = '<input type="checkbox" data-filename="' + filename + '" onchange="toggleAccountSelection(\'' + filename + '\', this.checked)"><span class="checkmark"></span>';
card.insertBefore(checkbox, card.firstChild);
}
} else {
if (checkbox) checkbox.remove();
card.classList.remove('selected');
}
});
updateBulkActionsBar();
}
function toggleAccountSelection(filename, checked) {
if (checked) {
selectedAccounts.add(filename);
} else {
selectedAccounts.delete(filename);
}
// Update visual state
const card = document.querySelector('.account[data-filename="' + filename + '"]');
if (card) card.classList.toggle('selected', checked);
updateBulkActionsBar();
}
function selectAllAccounts() {
document.querySelectorAll('.account-checkbox input').forEach(cb => {
cb.checked = true;
const filename = cb.dataset.filename;
if (filename) selectedAccounts.add(filename);
});
document.querySelectorAll('.account').forEach(card => card.classList.add('selected'));
updateBulkActionsBar();
}
function deselectAllAccounts() {
document.querySelectorAll('.account-checkbox input').forEach(cb => {
cb.checked = false;
});
document.querySelectorAll('.account').forEach(card => card.classList.remove('selected'));
selectedAccounts.clear();
updateBulkActionsBar();
}
function updateBulkActionsBar() {
const countEl = document.getElementById('bulkCount');
if (countEl) {
countEl.textContent = selectedAccounts.size.toString();
}
}
function exportSelectedAccounts() {
if (selectedAccounts.size === 0) return;
vscode.postMessage({ command: 'exportSelectedAccounts', filenames: Array.from(selectedAccounts) });
}
function refreshSelectedTokens() {
if (selectedAccounts.size === 0) return;
vscode.postMessage({ command: 'refreshSelectedTokens', filenames: Array.from(selectedAccounts) });
showToast(T.refreshingTokens || 'Refreshing tokens...', 'success');
}
function deleteSelectedAccounts() {
if (selectedAccounts.size === 0) return;
pendingAction = { type: 'deleteSelected', filenames: Array.from(selectedAccounts) };
document.getElementById('dialogTitle').textContent = T.deleteTitle;
document.getElementById('dialogText').textContent = (T.deleteSelectedConfirm || 'Delete {count} selected accounts?').replace('{count}', selectedAccounts.size);
// Reset button to Delete style
const btn = document.getElementById('dialogConfirmBtn');
btn.textContent = T.delete;
btn.className = 'btn btn-danger';
document.getElementById('dialogOverlay').classList.add('visible');
}
// === Init ===
document.addEventListener('DOMContentLoaded', () => {
// Scroll logs to bottom
const logsContent = document.getElementById('logsContent');
if (logsContent) logsContent.scrollTop = logsContent.scrollHeight;
// Load patch status on init
vscode.postMessage({ command: 'getPatchStatus' });
const savedTab = getState()?.activeTab;
if (savedTab && typeof savedTab === 'string') {
switchTab(savedTab);
}
});
// Export functions to window for onclick handlers
window.switchTab = switchTab;
window.openSettings = openSettings;
window.closeSettings = closeSettings;
window.toggleLogs = toggleLogs;
window.toggleBatchReg = toggleBatchReg;
window.clearConsole = clearConsole;
window.filterConsole = filterConsole;
window.copyLogs = copyLogs;
window.toggleAutoSwitch = toggleAutoSwitch;
window.toggleSetting = toggleSetting;
window.toggleSpoofing = toggleSpoofing;
window.changeLanguage = changeLanguage;
window.checkUpdates = checkUpdates;
window.exportAllAccounts = exportAllAccounts;
window.importAccounts = importAccounts;
window.confirmResetMachineId = confirmResetMachineId;
window.confirmPatchKiro = confirmPatchKiro;
window.confirmUnpatchKiro = confirmUnpatchKiro;
window.generateNewMachineId = generateNewMachineId;
window.openProfilesPanel = openProfilesPanel;
window.closeProfilesPanel = closeProfilesPanel;
window.createProfile = createProfile;
window.editProfile = editProfile;
window.deleteProfile = deleteProfile;
window.selectProfile = selectProfile;
window.closeProfileEditor = closeProfileEditor;
window.selectStrategy = selectStrategy;
window.onEmailInput = onEmailInput;
window.testImapConnection = testImapConnection;
window.addEmailToPool = addEmailToPool;
window.removeEmailFromPool = removeEmailFromPool;
window.importEmailsFromFile = importEmailsFromFile;
window.pasteEmails = pasteEmails;
window.handlePoolPaste = handlePoolPaste;
window.saveProfile = saveProfile;
window.togglePasswordVisibility = togglePasswordVisibility;
window.switchAccount = switchAccount;
window.refreshToken = refreshToken;
window.confirmDelete = confirmDelete;
window.copyToken = copyToken;
window.startAutoReg = startAutoReg;
window.startQuickAutoReg = startQuickAutoReg;
window.stopAutoReg = stopAutoReg;
window.togglePauseAutoReg = togglePauseAutoReg;
window.openSsoModal = openSsoModal;
window.closeSsoModal = closeSsoModal;
window.importSsoToken = importSsoToken;
window.refreshAllExpired = refreshAllExpired;
window.confirmDeleteExhausted = confirmDeleteExhausted;
window.confirmDeleteBanned = confirmDeleteBanned;
window.checkAllAccountsHealth = checkAllAccountsHealth;
window.showToast = showToast;
window.dialogAction = dialogAction;
window.closeDialog = closeDialog;
window.searchAccounts = searchAccounts;
window.clearSearch = clearSearch;
window.refresh = refresh;
window.refreshUsage = refreshUsage;
window.openUpdateUrl = openUpdateUrl;
window.openVsCodeSettings = openVsCodeSettings;
window.renderActiveProfile = renderActiveProfile;
window.toggleSelectionMode = toggleSelectionMode;
window.toggleAccountSelection = toggleAccountSelection;
window.selectAllAccounts = selectAllAccounts;
window.deselectAllAccounts = deselectAllAccounts;
window.exportSelectedAccounts = exportSelectedAccounts;
window.refreshSelectedTokens = refreshSelectedTokens;
window.deleteSelectedAccounts = deleteSelectedAccounts;
window.selectStrategy = selectStrategy;
window.selectRegistrationStrategy = selectRegistrationStrategy;
window.updateSetting = updateSetting;
window.showSkeletonLoading = showSkeletonLoading;
window.hideSkeletonLoading = hideSkeletonLoading;
window.renderSkeletonCards = renderSkeletonCards;
window.focusAccount = focusAccount;
window.switchTab = switchTab;
window.filterByTokens = filterByTokens;
window.filterConsole = filterConsole;
window.handleConsoleScroll = handleConsoleScroll;
window.scrollConsoleToBottom = scrollConsoleToBottom;
// === Toolbar More Dropdown ===
function toggleToolbarMore() {
const menu = document.getElementById('toolbarMoreMenu');
if (menu) {
menu.classList.toggle('visible');
// Close on click outside
if (menu.classList.contains('visible')) {
setTimeout(() => {
document.addEventListener('click', closeToolbarMoreOnClickOutside);
}, 10);
}
}
}
function closeToolbarMoreOnClickOutside(e) {
const menu = document.getElementById('toolbarMoreMenu');
const btn = document.querySelector('.toolbar-more-btn');
if (menu && !menu.contains(e.target) && !btn?.contains(e.target)) {
menu.classList.remove('visible');
document.removeEventListener('click', closeToolbarMoreOnClickOutside);
}
}
window.toggleToolbarMore = toggleToolbarMore;
// === Initialization ===
// Load profiles after DOM is ready so they're available when user switches to profiles tab
document.addEventListener('DOMContentLoaded', function() {
// Small delay to ensure webview message handler is ready
setTimeout(function() {
vscode.postMessage({ command: 'loadProfiles' });
}, 100);
});
// Fallback if DOMContentLoaded already fired
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(function() {
vscode.postMessage({ command: 'loadProfiles' });
}, 100);
}
// === Batch Registration ===
let scheduledRegTimer = null;
function updateScheduledRegSetting(key, value) {
vscode.postMessage({ command: 'updateScheduledRegSetting', key, value });
// Refresh preview when name mode changes
if (key === 'useCustomName' || key === 'customNamePrefix') {
vscode.postMessage({ command: 'refresh' });
}
}
function adjustBatchCount(delta) {
const input = document.getElementById('maxAccountsInput');
if (input) {
const current = parseInt(input.value) || 5;
const newVal = Math.max(1, Math.min(100, current + delta));
input.value = newVal;
updateScheduledRegSetting('maxAccounts', newVal);
}
}
function setBatchInterval(minutes) {
updateScheduledRegSetting('interval', minutes);
// Update pill buttons visually
document.querySelectorAll('.interval-pill').forEach(pill => {
pill.classList.toggle('active', parseInt(pill.getAttribute('onclick').match(/\d+/)?.[0] || '0') === minutes);
});
// Update hint
vscode.postMessage({ command: 'refresh' });
}
function startScheduledReg() {
// Auto-enable when starting
updateScheduledRegSetting('enabled', true);
vscode.postMessage({ command: 'startScheduledReg' });
}
function stopScheduledReg() {
vscode.postMessage({ command: 'stopScheduledReg' });
if (scheduledRegTimer) {
clearInterval(scheduledRegTimer);
scheduledRegTimer = null;
}
}
function resetScheduledReg() {
vscode.postMessage({ command: 'resetScheduledReg' });
}
function updateScheduledRegTimer(nextRunAt) {
const timerEl = document.getElementById('scheduledRegTimer');
if (!timerEl || !nextRunAt) return;
if (scheduledRegTimer) {
clearInterval(scheduledRegTimer);
}
scheduledRegTimer = setInterval(() => {
const now = Date.now();
const next = new Date(nextRunAt).getTime();
const diff = Math.max(0, next - now);
if (diff <= 0) {
timerEl.textContent = '00:00';
clearInterval(scheduledRegTimer);
scheduledRegTimer = null;
return;
}
const minutes = Math.floor(diff / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
timerEl.textContent = minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
}, 1000);
}
function updateScheduledRegState(state) {
const card = document.getElementById('scheduledRegCard');
if (!card) return;
// Toggle running class on card
card.classList.toggle('running', state.isRunning);
// Update progress ring if running
const progressFill = card.querySelector('.progress-fill');
if (progressFill && state.maxAccounts > 0) {
const percent = Math.round((state.registeredCount / state.maxAccounts) * 100);
progressFill.setAttribute('stroke-dasharray', Math.min(100, percent) + ', 100');
}
// Update progress text
const progressText = card.querySelector('.progress-text');
if (progressText) {
progressText.textContent = state.registeredCount + '/' + state.maxAccounts;
}
// Update timer countdown
if (state.nextRunAt && state.isRunning && state.interval > 0) {
updateScheduledRegTimer(state.nextRunAt);
} else if (scheduledRegTimer) {
clearInterval(scheduledRegTimer);
scheduledRegTimer = null;
const timerEl = document.getElementById('scheduledRegTimer');
if (timerEl) timerEl.textContent = '--:--';
}
// Full refresh for state changes (running/stopped)
if (state._refresh) {
vscode.postMessage({ command: 'refresh' });
}
}
// Export batch reg functions
window.updateScheduledRegSetting = updateScheduledRegSetting;
window.adjustBatchCount = adjustBatchCount;
window.setBatchInterval = setBatchInterval;
window.startScheduledReg = startScheduledReg;
window.stopScheduledReg = stopScheduledReg;
window.resetScheduledReg = resetScheduledReg;
window.updateScheduledRegState = updateScheduledRegState;
</script>
</body>
</html>