Spaces:
Paused
Paused
| <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 ; } | |
| .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 ; } | |
| } | |
| /* === 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 ; | |
| flex-wrap: nowrap ; | |
| } | |
| .console-time { | |
| display: none; | |
| } | |
| .console-count { | |
| display: none; | |
| } | |
| .console-msg { | |
| white-space: normal ; | |
| writing-mode: horizontal-tb ; | |
| word-break: break-all ; | |
| } | |
| } | |
| @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 ; | |
| text-orientation: mixed ; | |
| } | |
| /* === 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 ; } | |
| .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 ; | |
| } | |
| /* 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 ; | |
| } | |
| /* 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 ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| /* === Print Styles === */ | |
| @media print { | |
| .console-drawer, | |
| .toast-container, | |
| .modal-overlay, | |
| .dialog-overlay { | |
| display: none ; | |
| } | |
| 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"><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( | |
| /("[^&]*"|'[^&]*'|"[^"]*"|'[^']*')/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> | |