302 lines
7.1 KiB
Svelte
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>
|