password_manager/src/components/PasswordGenerator.svelte
2026-05-11 22:32:05 +00:00

302 lines
7.1 KiB
Svelte

<script>
import { generatePassword } from '../lib/crypto/crypto.js'
let length = $state(16)
let uppercase = $state(true)
let lowercase = $state(true)
let digits = $state(true)
let symbols = $state(true)
let exclude = $state('')
let generated = $state('')
let strength = $state('')
let toast = $state('')
let toastTimer = null
// Generate on mount and whenever settings change
// $effect auto-tracks all reactive reads
$effect(() => {
// Reading these values registers them as dependencies
const _l = length
const _u = uppercase
const _lo = lowercase
const _d = digits
const _s = symbols
const _e = exclude
regenerate()
})
function regenerate() {
try {
generated = generatePassword({ length, uppercase, lowercase, digits, symbols, exclude })
strength = calculateStrength(generated)
} catch (e) {
generated = ''
strength = ''
}
}
function calculateStrength(password) {
if (!password) return { label: '', color: '' }
let score = 0
if (password.length >= 8) score++
if (password.length >= 12) score++
if (password.length >= 16) score++
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++
if (/\d/.test(password)) score++
if (/[^a-zA-Z0-9]/.test(password)) score++
if (score <= 2) return { label: 'Weak', color: 'var(--color-danger)' }
if (score <= 4) return { label: 'Moderate', color: 'var(--color-warning)' }
return { label: 'Strong', color: 'var(--color-success)' }
}
function showToast(message) {
toast = message
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => { toast = '' }, 2000)
}
async function copyPassword() {
try {
await navigator.clipboard.writeText(generated)
showToast('✓ Password copied to clipboard')
} catch {
// Fallback
const textarea = document.createElement('textarea')
textarea.value = generated
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
showToast('✓ Password copied')
}
}
</script>
<div class="password-generator">
{#if toast}
<div class="toast">{toast}</div>
{/if}
<div class="generator-card">
<h3>🔑 Password Generator</h3>
<!-- Generated password display -->
<div class="generated-display">
<code class="generated-password">{generated}</code>
<div class="display-actions">
<span class="strength-badge" style="color: {strength.color}">{strength.label}</span>
<button class="btn btn-ghost btn-sm" onclick={copyPassword} title="Copy">📋 Copy</button>
<button class="btn btn-ghost btn-sm" onclick={regenerate} title="Regenerate">🔄</button>
</div>
</div>
<!-- Length slider -->
<div class="form-group">
<div class="slider-header">
<span class="field-label">Length</span>
<span class="slider-value">{length}</span>
</div>
<input
type="range"
min="4"
max="64"
bind:value={length}
class="length-slider"
/>
<div class="slider-marks">
<span>4</span>
<span>16</span>
<span>32</span>
<span>64</span>
</div>
</div>
<!-- Character types -->
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" bind:checked={uppercase} />
<span>Uppercase (A-Z)</span>
</label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={lowercase} />
<span>Lowercase (a-z)</span>
</label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={digits} />
<span>Numbers (0-9)</span>
</label>
<label class="checkbox-label">
<input type="checkbox" bind:checked={symbols} />
<span>Symbols (!@#$...)</span>
</label>
</div>
<!-- Exclude characters -->
<div class="form-group">
<label for="exclude-chars">Exclude characters</label>
<input
id="exclude-chars"
type="text"
bind:value={exclude}
placeholder="e.g. 0OIl1"
/>
</div>
</div>
</div>
<style>
.toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 16px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
font-size: 0.85rem;
color: var(--color-success);
box-shadow: var(--shadow);
z-index: 1000;
}
.generator-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 20px;
max-width: 450px;
}
.generator-card h3 {
margin-bottom: 16px;
}
.generated-display {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
background: var(--color-input-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
margin-bottom: 16px;
flex-wrap: wrap;
}
.generated-password {
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 1rem;
word-break: break-all;
flex: 1;
min-width: 0;
}
.display-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.strength-badge {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.field-label {
font-size: 0.8rem;
font-weight: 500;
color: var(--color-text-muted);
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.slider-value {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-primary);
}
.length-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: var(--color-border);
border-radius: 3px;
outline: none;
border: none;
padding: 0;
}
.length-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-primary);
cursor: pointer;
}
.length-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-primary);
cursor: pointer;
border: none;
}
.slider-marks {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--color-text-muted);
margin-top: 4px;
padding: 0 2px;
}
.checkbox-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
cursor: pointer;
padding: 6px 8px;
border-radius: var(--radius-sm);
transition: background-color 150ms;
}
.checkbox-label:hover {
background: var(--color-surface-hover);
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--color-primary);
cursor: pointer;
}
.checkbox-label span {
flex: 1;
}
</style>