Password Generator Widget
blocksecurity1Password and Bitwarden-style customizable password and passphrase generator with entropy strength meter, character type toggles, memorable Diceware passphrases, PIN codes, and volatile history drawer.
Installation
$pnpm dlx uipkge-ng@latest add password-generator-widget$npx uipkge-ng@latest add password-generator-widget$yarn dlx uipkge-ng@latest add password-generator-widget$bunx uipkge-ng@latest add password-generator-widgetManifest:
https://uipkge.dev/r/angular/password-generator-widget.jsonInstalls to:src/app/components/blocks/password-generator-widget/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HistoryIteminterface HistoryItem {
id: string
secret: string
mode: GeneratorMode
entropy: number
strengthLabel: string
createdAt: string
}FormattedCharinterface FormattedChar {
char: string
type: 'number' | 'symbol' | 'letter' | 'separator'
}Files installed (2)
src/app/components/blocks/password-generator-widget/password-generator-widget.component.ts40.4 kB
// 1Password / Bitwarden style customizable password, Diceware passphrase and PIN // generator: cryptographic output with syntax highlighting, entropy strength meter with // crack-time estimate, per-mode configuration tabs (length slider + presets, character // pools, separator grid, PIN rules), and a volatile masked history drawer. Port of the // React block 1:1. Option changes regenerate quietly (no history spam); Regenerate logs. import { ChangeDetectionStrategy, Component, Input, OnInit, computed, signal } from '@angular/core' import { Check, Clock, Copy, Dice5, Eye, EyeOff, Hash, History, KeyRound, Lock, LucideAngularModule, RefreshCw, Shield, ShieldCheck, Sliders, Trash2, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export type GeneratorMode = 'password' | 'passphrase' | 'pin' export interface HistoryItem { id: string secret: string mode: GeneratorMode entropy: number strengthLabel: string createdAt: string } const WORD_LIST = [ 'anchor', 'apple', 'apron', 'arctic', 'arrow', 'atlas', 'autumn', 'bacon', 'badge', 'banner', 'beacon', 'blanket', 'breeze', 'bridge', 'cabin', 'cactus', 'camera', 'candle', 'canyon', 'carpet', 'castle', 'cedar', 'celestial', 'channel', 'cherry', 'chimney', 'cipher', 'circle', 'clay', 'cliff', 'clock', 'cloud', 'clover', 'cobalt', 'coffee', 'comet', 'compass', 'copper', 'coral', 'cosmos', 'crater', 'creek', 'crystal', 'curtain', 'delta', 'desert', 'diamond', 'dolphin', 'dragon', 'drift', 'eagle', 'earth', 'echo', 'eclipse', 'ember', 'emerald', 'engine', 'falcon', 'feather', 'fender', 'filter', 'flame', 'flask', 'forest', 'fossil', 'galaxy', 'garden', 'garlic', 'garnet', 'gem', 'glacier', 'glass', 'globe', 'granite', 'gravel', 'grove', 'harbor', 'haven', 'hawk', 'hazel', 'horizon', 'island', 'ivory', 'jungle', 'jupiter', 'kettle', 'lagoon', 'lantern', 'laser', 'lava', 'leaf', 'lemon', 'leopard', 'lighthouse', 'lightning', 'lotus', 'lunar', 'magnet', 'mango', 'maple', 'marble', 'matrix', 'meadow', 'meteor', 'mineral', 'mirror', 'monarch', 'moon', 'mosaic', 'mountain', 'nebula', 'nickel', 'oasis', 'ocean', 'olive', 'onyx', 'opal', 'orbit', 'orchid', 'oxygen', 'palace', 'panther', 'paradise', 'pebble', 'pelican', 'peacock', 'phoenix', 'pillar', 'planet', 'plasma', 'platinum', 'polar', 'prism', 'pulse', 'pyramid', 'quartz', 'quiver', 'radar', 'rainbow', 'raven', 'reef', 'ridge', 'ripple', 'river', 'rocket', 'ruby', 'safari', 'sail', 'sapphire', 'saturn', 'shadow', 'shield', 'sierra', 'silver', 'solstice', 'spark', 'sphere', 'spiral', 'spring', 'spruce', 'star', 'stone', 'summit', 'sunset', 'surge', 'timber', 'topaz', 'torch', 'trail', 'treasure', 'tundra', 'valley', 'vector', 'velvet', 'vessel', 'vintage', 'violet', 'vortex', 'voyage', 'vulcan', 'walnut', 'willow', 'wind', 'winter', 'zenith', 'zephyr', ] const UPPERCASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' const LOWERCASE_CHARS = 'abcdefghijklmnopqrstuvwxyz' const NUMBER_CHARS = '0123456789' const SYMBOL_CHARS = '!@#$%^&*()_+-=[]{}|;:,.<>?' const AMBIGUOUS_CHARS = new Set(['l', '1', 'I', 'O', '0', 'o', '|', '`', "'", '"', ',', ';']) const PASSWORD_PRESETS = [12, 16, 20, 24, 32, 48, 64] const WORD_PRESETS = [3, 4, 5, 6, 8] const PIN_PRESETS = [4, 6, 8, 10, 12, 16] const SEPARATORS = [ { id: '-', label: 'Hyphen (-)' }, { id: '_', label: 'Under (_)' }, { id: '.', label: 'Period (.)' }, { id: ' ', label: 'Space ( )' }, { id: '/', label: 'Slash (/)' }, ] export interface FormattedChar { char: string type: 'number' | 'symbol' | 'letter' | 'separator' } function classifyChar(char: string): FormattedChar['type'] { if (/[0-9]/.test(char)) return 'number' if (/[-_. /]/.test(char)) return 'separator' if (/[^a-zA-Z0-9]/.test(char)) return 'symbol' return 'letter' } function getRandomInt(max: number): number { if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { const array = new Uint32Array(1) window.crypto.getRandomValues(array) return array[0]! % max } return Math.floor(Math.random() * max) } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-password-generator-widget, [ui-password-generator-widget]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, UiSliderComponent, UiSwitchComponent, UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div data-slot="password-generator-widget" [class]="rootClass"> <!-- HERO: GENERATOR OUTPUT CARD --> <div ui-card class="overflow-hidden shadow-xs"> <div ui-card-header class="border-border/50 bg-muted/20 border-b pb-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="KeyRound" class="size-4" /> </div> <div> <h3 ui-card-title class="text-base font-semibold">Generated Secret</h3> <p ui-card-description class="text-xs">Client-side cryptographic output & entropy estimation</p> </div> </div> <div class="flex items-center gap-2"> <span ui-badge variant="outline" class="gap-1.5 text-xs font-normal"> <span class="bg-success size-2 animate-pulse rounded-full"></span> 100% Client-Side </span> </div> </div> </div> <div ui-card-content class="space-y-6 pt-6"> <div class="border-border/80 bg-muted/40 dark:bg-muted/20 relative flex min-h-[4.75rem] w-full flex-col justify-center gap-4 rounded-xl border p-4 sm:flex-row sm:items-center sm:justify-between" > <div class="flex flex-wrap items-center gap-x-0.5 gap-y-1 pr-2 font-mono text-lg font-medium tracking-wider break-all select-all md:text-xl" > @if (formattedCharacters().length > 0) { @for (item of formattedCharacters(); track $index) { <span [class]="charClass(item.type)">{{ item.char }}</span> } } @else { <span class="text-muted-foreground text-sm">Generating secret...</span> } </div> <div class="flex shrink-0 items-center gap-2 self-end sm:self-center"> <button ui-button variant="outline" size="sm" class="h-9 gap-1.5 px-3 active:scale-95" (click)="handleManualRegenerate()" > <lucide-icon [img]="RefreshCw" [class]="spinClass" /> <span class="text-xs font-medium">Regenerate</span> </button> <button ui-button variant="default" size="sm" [class]="copyClass" (click)="copySecret(currentSecret())"> @if (copied()) { <lucide-icon [img]="Check" class="size-4 stroke-[2.5]" /> } @else { <lucide-icon [img]="Copy" class="size-4" /> } <span class="text-xs font-medium">{{ copied() ? 'Copied!' : 'Copy' }}</span> </button> </div> </div> <!-- Entropy & Strength Meter --> <div class="border-border/60 bg-muted/10 space-y-2.5 rounded-lg border p-4"> <div class="flex flex-wrap items-center justify-between gap-2 text-xs"> <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="size-4" [class]="currentStrength().color" /> <span class="text-foreground font-semibold">{{ currentStrength().label }}</span> <span class="text-muted-foreground font-mono">{{ currentStrength().entropy }}-bit entropy</span> </div> <div class="text-muted-foreground flex items-center gap-1.5"> <lucide-icon [img]="Clock" class="size-3.5" /> <span>Crack time:</span> <span class="text-foreground font-medium">{{ currentStrength().crackTime }}</span> </div> </div> <div class="grid h-2 w-full grid-cols-4 gap-1.5 overflow-hidden rounded-full"> <div class="rounded-full transition-colors duration-300" [class]="segClass(1)"></div> <div class="rounded-full transition-colors duration-300" [class]="segClass(2)"></div> <div class="rounded-full transition-colors duration-300" [class]="segClass(3)"></div> <div class="rounded-full transition-colors duration-300" [class]="segClass(4)"></div> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-4 pt-1 text-xs"> <span class="flex items-center gap-1.5"> <span class="bg-foreground size-2 rounded-full"></span> Letters </span> <span class="flex items-center gap-1.5"> <span class="bg-info size-2 rounded-full"></span> Numbers (0-9) </span> <span class="flex items-center gap-1.5"> <span class="bg-warning size-2 rounded-full"></span> Symbols (!@#$) </span> </div> </div> </div> </div> <!-- GENERATOR CONFIGURATION CARD --> <div ui-card class="shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Sliders" class="size-4" /> </div> <div> <h3 ui-card-title class="text-base font-semibold">Generator Configuration</h3> <p ui-card-description class="text-xs"> Customize character pools, length constraints, and format patterns </p> </div> </div> </div> <div ui-card-content class="pt-6"> <ui-tabs [value]="mode()" (valueChange)="setMode($event)" class="w-full"> <ui-tabs-list class="mb-6 grid w-full grid-cols-3"> <button ui-tabs-trigger value="password" class="gap-1.5 text-xs"> <lucide-icon [img]="Lock" class="size-3.5" /> <span>Random Password</span> </button> <button ui-tabs-trigger value="passphrase" class="gap-1.5 text-xs"> <lucide-icon [img]="Dice5" class="size-3.5" /> <span>Passphrase</span> </button> <button ui-tabs-trigger value="pin" class="gap-1.5 text-xs"> <lucide-icon [img]="Hash" class="size-3.5" /> <span>PIN Code</span> </button> </ui-tabs-list> <!-- TAB 1: RANDOM PASSWORD --> <ui-tabs-content value="password" class="mt-0 space-y-6"> <div class="space-y-3"> <div class="flex items-center justify-between"> <label class="text-foreground text-sm font-medium">Password Length</label> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold tabular-nums"> {{ passwordLength() }} characters </span> </div> <ui-slider [value]="[passwordLength()]" [min]="8" [max]="64" [step]="1" class="w-full py-1" (valueChange)="setPasswordLength($event[0] ?? passwordLength())" /> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground mr-1 text-xs">Presets:</span> @for (preset of passwordPresets; track preset) { <button type="button" [class]="presetClass(passwordLength() === preset)" (click)="setPasswordLength(preset)" > {{ preset }} </button> } </div> </div> <ui-separator /> <div class="space-y-3"> <label class="text-muted-foreground text-xs font-medium"> Character Pools </label> <div class="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Uppercase Letters</span> <span class="text-muted-foreground font-mono text-xs font-normal">(A-Z)</span> </div> <p class="text-muted-foreground text-xs">Include capital Latin characters</p> </div> <ui-switch [checked]="includeUpper()" (checkedChange)="toggleUpper($event)" /> </div> <ui-separator /> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Lowercase Letters</span> <span class="text-muted-foreground font-mono text-xs font-normal">(a-z)</span> </div> <p class="text-muted-foreground text-xs">Include small Latin characters</p> </div> <ui-switch [checked]="includeLower()" (checkedChange)="toggleLower($event)" /> </div> <ui-separator /> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Numbers</span> <span class="text-info font-mono text-xs font-semibold">(0-9)</span> </div> <p class="text-muted-foreground text-xs">Include numeric digits 0 through 9</p> </div> <ui-switch [checked]="includeNumbers()" (checkedChange)="toggleNumbers($event)" /> </div> <ui-separator /> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Special Symbols</span> <span class="text-warning font-mono text-xs font-semibold">(!@#$%^&*)</span> </div> <p class="text-muted-foreground text-xs">Include non-alphanumeric punctuation marks</p> </div> <ui-switch [checked]="includeSymbols()" (checkedChange)="toggleSymbols($event)" /> </div> <ui-separator /> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Avoid Ambiguous Characters</span> <span class="text-muted-foreground font-mono text-xs font-normal">(l, 1, I, O, 0)</span> </div> <p class="text-muted-foreground text-xs">Exclude confusing lookalike letters and digits</p> </div> <ui-switch [checked]="avoidAmbiguous()" (checkedChange)="setAvoidAmbiguous($event)" /> </div> </div> </div> </ui-tabs-content> <!-- TAB 2: MEMORABLE PASSPHRASE --> <ui-tabs-content value="passphrase" class="mt-0 space-y-6"> <div class="space-y-3"> <div class="flex items-center justify-between"> <label class="text-foreground text-sm font-medium">Number of Words</label> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold tabular-nums"> {{ wordCount() }} words </span> </div> <ui-slider [value]="[wordCount()]" [min]="3" [max]="8" [step]="1" class="w-full py-1" (valueChange)="setWordCount($event[0] ?? wordCount())" /> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground mr-1 text-xs">Presets:</span> @for (preset of wordPresets; track preset) { <button type="button" [class]="presetClass(wordCount() === preset)" (click)="setWordCount(preset)"> {{ preset }} words </button> } </div> </div> <ui-separator /> <div class="space-y-3"> <label class="text-muted-foreground text-xs font-medium"> Word Separator </label> <div class="grid grid-cols-5 gap-2"> @for (sep of separators; track sep.id) { <button type="button" [class]="separatorClass(separator() === sep.id)" (click)="setSeparator(sep.id)" > <span class="font-mono text-sm font-semibold">{{ sep.id === ' ' ? '␣' : sep.id }}</span> <span class="mt-0.5 truncate text-xs">{{ sepLabel(sep.label) }}</span> </button> } </div> </div> <ui-separator /> <div class="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground text-sm font-medium">Capitalize Words</div> <p class="text-muted-foreground text-xs">Transform each word into TitleCase for easy reading</p> </div> <ui-switch [checked]="capitalizeWords()" (checkedChange)="setCapitalizeWords($event)" /> </div> <ui-separator /> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground text-sm font-medium">Append Random Number</div> <p class="text-muted-foreground text-xs">Add a 2-digit number (10-99) for additional security</p> </div> <ui-switch [checked]="includeNumberInPassphrase()" (checkedChange)="setIncludeNumberInPassphrase($event)" /> </div> </div> </ui-tabs-content> <!-- TAB 3: PIN CODE --> <ui-tabs-content value="pin" class="mt-0 space-y-6"> <div class="space-y-3"> <div class="flex items-center justify-between"> <label class="text-foreground text-sm font-medium">PIN Length</label> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold tabular-nums"> {{ pinLength() }} digits </span> </div> <ui-slider [value]="[pinLength()]" [min]="4" [max]="16" [step]="1" class="w-full py-1" (valueChange)="setPinLength($event[0] ?? pinLength())" /> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground mr-1 text-xs">Presets:</span> @for (preset of pinPresets; track preset) { <button type="button" [class]="presetClass(pinLength() === preset)" (click)="setPinLength(preset)"> {{ preset }} digits </button> } </div> </div> <ui-separator /> <div class="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <div class="space-y-0.5"> <div class="text-foreground text-sm font-medium">Avoid Consecutive Repeated Digits</div> <p class="text-muted-foreground text-xs"> Prevent obvious repetitive patterns like “1122” or “8888” </p> </div> <ui-switch [checked]="avoidPinRepeats()" (checkedChange)="setAvoidPinRepeats($event)" /> </div> </div> </ui-tabs-content> </ui-tabs> </div> </div> <!-- RECENT HISTORY CARD --> <div ui-card class="shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="History" class="size-4" /> </div> <div> <h3 ui-card-title class="text-base font-semibold">Generated History</h3> <p ui-card-description class="text-xs"> Stored in volatile browser memory only • Cleared on page refresh </p> </div> </div> @if (history().length > 0) { <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-destructive h-8 gap-1.5 text-xs" (click)="clearHistory()" > <lucide-icon [img]="Trash2" class="size-3.5" /> <span>Clear History</span> </button> } </div> </div> <div ui-card-content class="pt-4"> @if (history().length === 0) { <div class="text-muted-foreground py-8 text-center text-xs"> No recent secrets in session history. Regenerate or copy above to log items here. </div> } @else { <div class="divide-border/60 divide-y"> @for (item of history(); track item.id) { <div class="group hover:bg-muted/30 flex flex-col justify-between gap-3 rounded-md px-1 py-3 transition-colors sm:flex-row sm:items-center" > <div class="flex min-w-0 items-center gap-3"> <div class="bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-md" > @if (item.mode === 'password') { <lucide-icon [img]="Lock" class="size-3.5" /> } @else if (item.mode === 'passphrase') { <lucide-icon [img]="Dice5" class="size-3.5" /> } @else { <lucide-icon [img]="Hash" class="size-3.5" /> } </div> <div class="min-w-0 space-y-1"> <div class="flex items-center gap-2"> <div class="max-w-[18rem] truncate font-mono text-xs font-medium md:max-w-md"> @if (isRevealed(item.id)) { @for (sub of formatItem(item.secret); track $index) { <span [class]="charClass(sub.type)">{{ sub.char }}</span> } } @else { <span class="text-muted-foreground tracking-widest">••••••••••••••••</span> } </div> <button type="button" class="text-muted-foreground hover:text-foreground cursor-pointer p-0.5 transition-colors" title="Toggle Visibility" (click)="toggleHistoryReveal(item.id)" > @if (isRevealed(item.id)) { <lucide-icon [img]="EyeOff" class="size-3.5" /> } @else { <lucide-icon [img]="Eye" class="size-3.5" /> } </button> </div> <div class="text-muted-foreground flex items-center gap-2 text-xs"> <span ui-badge variant="outline" class="px-1.5 py-0 text-xs font-normal"> {{ item.mode }} </span> <span>•</span> <span>{{ item.entropy }}-bit ({{ item.strengthLabel }})</span> <span>•</span> <span>{{ item.createdAt }}</span> </div> </div> </div> <div class="flex shrink-0 items-center gap-2 self-end sm:self-center"> <button ui-button variant="outline" size="sm" [class]="historyCopyClass(item.id)" (click)="copySecret(item.secret, true, item.id)" > @if (copiedHistoryId() === item.id) { <lucide-icon [img]="Check" class="size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ copiedHistoryId() === item.id ? 'Copied' : 'Copy' }}</span> </button> </div> </div> } </div> } </div> <div ui-card-footer class="text-muted-foreground border-border/40 flex items-center justify-between border-t pt-3 pb-4 text-xs" > <span class="flex items-center gap-1.5"> <lucide-icon [img]="Shield" class="text-success size-3.5" /> Cryptographically random • Never transmitted over network </span> <span class="font-mono tabular-nums">{{ history().length }} entries</span> </div> </div> </div> `, }) export class UiPasswordGeneratorWidgetComponent implements OnInit { protected readonly Check = Check protected readonly Clock = Clock protected readonly Copy = Copy protected readonly Dice5 = Dice5 protected readonly Eye = Eye protected readonly EyeOff = EyeOff protected readonly Hash = Hash protected readonly History = History protected readonly KeyRound = KeyRound protected readonly Lock = Lock protected readonly RefreshCw = RefreshCw protected readonly Shield = Shield protected readonly ShieldCheck = ShieldCheck protected readonly Sliders = Sliders protected readonly Trash2 = Trash2 protected readonly passwordPresets = PASSWORD_PRESETS protected readonly wordPresets = WORD_PRESETS protected readonly pinPresets = PIN_PRESETS protected readonly separators = SEPARATORS @Input('class') className?: string readonly mode = signal<GeneratorMode>('password') readonly passwordLength = signal(20) readonly includeUpper = signal(true) readonly includeLower = signal(true) readonly includeNumbers = signal(true) readonly includeSymbols = signal(true) readonly avoidAmbiguous = signal(true) readonly wordCount = signal(5) readonly separator = signal('-') readonly capitalizeWords = signal(true) readonly includeNumberInPassphrase = signal(true) readonly pinLength = signal(6) readonly avoidPinRepeats = signal(true) readonly currentSecret = signal('') readonly copied = signal(false) readonly isSpinning = signal(false) readonly copiedHistoryId = signal<string | null>(null) readonly revealed = signal<Record<string, boolean>>({}) readonly history = signal<HistoryItem[]>([ { id: 'hist-1', secret: 'kX9#mQ4$vL2!pW8*jT7^', mode: 'password', entropy: 114, strengthLabel: 'Very Strong', createdAt: 'Just now', }, { id: 'hist-2', secret: 'Celestial-Falcon-Lagoon-Timber-83', mode: 'passphrase', entropy: 48, strengthLabel: 'Fair', createdAt: '4m ago', }, { id: 'hist-3', secret: '928401', mode: 'pin', entropy: 20, strengthLabel: 'Very Weak', createdAt: '12m ago', }, ]) ngOnInit(): void { this.regenerateSecret(false) } readonly currentStrength = computed(() => { let entropy = 0 if (this.mode() === 'password') { let poolSize = 0 if (this.includeUpper()) poolSize += this.avoidAmbiguous() ? 24 : 26 if (this.includeLower()) poolSize += this.avoidAmbiguous() ? 24 : 26 if (this.includeNumbers()) poolSize += this.avoidAmbiguous() ? 8 : 10 if (this.includeSymbols()) poolSize += this.avoidAmbiguous() ? 26 : 28 entropy = poolSize > 1 ? Math.round(this.passwordLength() * Math.log2(poolSize)) : 0 } else if (this.mode() === 'passphrase') { entropy = this.wordCount() * Math.log2(WORD_LIST.length) if (this.capitalizeWords()) entropy += this.wordCount() * 1 if (this.includeNumberInPassphrase()) entropy += Math.log2(90) entropy = Math.round(entropy) } else { entropy = Math.round(this.pinLength() * Math.log2(10)) } if (entropy < 30) { return { entropy, score: 1, label: 'Very Weak', crackTime: 'Instant (< 1 ms)', color: 'text-destructive', barClass: 'bg-destructive', } } if (entropy < 50) { return { entropy, score: 2, label: 'Weak', crackTime: 'A few minutes', color: 'text-orange-500', barClass: 'bg-orange-500', } } if (entropy < 70) { return { entropy, score: 3, label: 'Fair', crackTime: 'Several months', color: 'text-warning', barClass: 'bg-warning', } } if (entropy < 95) { return { entropy, score: 4, label: 'Strong', crackTime: 'Hundreds of years', color: 'text-success', barClass: 'bg-success', } } return { entropy, score: 4, label: 'Very Strong', crackTime: '100+ billion years', color: 'text-success', barClass: 'bg-success', } }) readonly formattedCharacters = computed<FormattedChar[]>(() => { const secret = this.currentSecret() if (!secret) return [] return secret.split('').map((char) => ({ char, type: classifyChar(char) })) }) get rootClass(): string { return cn('mx-auto w-full max-w-4xl space-y-6', this.className) } get spinClass(): string { return cn('size-4', this.isSpinning() && 'text-primary animate-spin') } get copyClass(): string { return cn('h-9 gap-1.5 px-3.5 active:scale-95', this.copied() && 'bg-success hover:bg-success text-white') } charClass(type: FormattedChar['type']): string { if (type === 'number') return 'text-info font-semibold' if (type === 'symbol') return 'text-warning font-semibold' if (type === 'separator') return 'text-muted-foreground px-0.5 font-normal' return 'text-foreground font-medium' } segClass(index: number): string { return this.currentStrength().score >= index ? this.currentStrength().barClass : 'bg-muted/80 dark:bg-muted' } presetClass(active: boolean): string { return cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', active ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', ) } separatorClass(active: boolean): string { return cn( 'flex cursor-pointer flex-col items-center justify-center rounded-lg border p-2.5 text-xs font-medium transition-colors', active ? 'bg-primary/10 border-primary text-primary font-semibold shadow-xs' : 'border-border bg-background hover:bg-muted/50 text-muted-foreground hover:text-foreground', ) } sepLabel(label: string): string { return label.split(' ')[0]! } historyCopyClass(id: string): string { return cn( 'h-8 gap-1.5 px-2.5 text-xs active:scale-95', this.copiedHistoryId() === id && 'bg-success hover:bg-success text-white', ) } formatItem(text: string): FormattedChar[] { return text.split('').map((char) => ({ char, type: classifyChar(char) })) } isRevealed(id: string): boolean { return !!this.revealed()[id] } setMode(value: string): void { this.mode.set(value as GeneratorMode) this.regenerateSecret(false) } setPasswordLength(value: number): void { this.passwordLength.set(value) this.regenerateSecret(false) } setWordCount(value: number): void { this.wordCount.set(value) this.regenerateSecret(false) } setPinLength(value: number): void { this.pinLength.set(value) this.regenerateSecret(false) } setSeparator(value: string): void { this.separator.set(value) this.regenerateSecret(false) } setCapitalizeWords(value: boolean): void { this.capitalizeWords.set(value) this.regenerateSecret(false) } setIncludeNumberInPassphrase(value: boolean): void { this.includeNumberInPassphrase.set(value) this.regenerateSecret(false) } setAvoidAmbiguous(value: boolean): void { this.avoidAmbiguous.set(value) this.regenerateSecret(false) } setAvoidPinRepeats(value: boolean): void { this.avoidPinRepeats.set(value) this.regenerateSecret(false) } // At least one character pool stays active in password mode. toggleUpper(val: boolean): void { if (!val && !this.includeLower() && !this.includeNumbers() && !this.includeSymbols()) return this.includeUpper.set(val) this.regenerateSecret(false) } toggleLower(val: boolean): void { if (!val && !this.includeUpper() && !this.includeNumbers() && !this.includeSymbols()) return this.includeLower.set(val) this.regenerateSecret(false) } toggleNumbers(val: boolean): void { if (!val && !this.includeUpper() && !this.includeLower() && !this.includeSymbols()) return this.includeNumbers.set(val) this.regenerateSecret(false) } toggleSymbols(val: boolean): void { if (!val && !this.includeUpper() && !this.includeLower() && !this.includeNumbers()) return this.includeSymbols.set(val) this.regenerateSecret(false) } generatePassword(): string { let pool = '' const requiredChars: string[] = [] const filterPool = (chars: string): string => { if (!this.avoidAmbiguous()) return chars return chars .split('') .filter((c) => !AMBIGUOUS_CHARS.has(c)) .join('') } const uppers = filterPool(UPPERCASE_CHARS) const lowers = filterPool(LOWERCASE_CHARS) const numbers = filterPool(NUMBER_CHARS) const symbols = filterPool(SYMBOL_CHARS) if (this.includeUpper() && uppers.length > 0) { pool += uppers requiredChars.push(uppers[getRandomInt(uppers.length)]!) } if (this.includeLower() && lowers.length > 0) { pool += lowers requiredChars.push(lowers[getRandomInt(lowers.length)]!) } if (this.includeNumbers() && numbers.length > 0) { pool += numbers requiredChars.push(numbers[getRandomInt(numbers.length)]!) } if (this.includeSymbols() && symbols.length > 0) { pool += symbols requiredChars.push(symbols[getRandomInt(symbols.length)]!) } if (pool.length === 0) { pool = lowers.length > 0 ? lowers : 'abcdefghjkmnpqrstuvwxyz' requiredChars.push(pool[getRandomInt(pool.length)]!) } const result: string[] = [...requiredChars] for (let i = result.length; i < this.passwordLength(); i++) { result.push(pool[getRandomInt(pool.length)]!) } for (let i = result.length - 1; i > 0; i--) { const j = getRandomInt(i + 1) const temp = result[i]! result[i] = result[j]! result[j] = temp } return result.join('') } generatePassphrase(): string { const chosenWords: string[] = [] for (let i = 0; i < this.wordCount(); i++) { let word = WORD_LIST[getRandomInt(WORD_LIST.length)]! if (this.capitalizeWords()) word = word.charAt(0).toUpperCase() + word.slice(1) chosenWords.push(word) } if (this.includeNumberInPassphrase()) { chosenWords.push((getRandomInt(90) + 10).toString()) } return chosenWords.join(this.separator()) } generatePin(): string { const digits: string[] = [] for (let i = 0; i < this.pinLength(); i++) { let nextDigit = getRandomInt(10).toString() if (this.avoidPinRepeats() && digits.length > 0 && nextDigit === digits[digits.length - 1]) { nextDigit = ((parseInt(nextDigit, 10) + 1 + getRandomInt(8)) % 10).toString() } digits.push(nextDigit) } return digits.join('') } regenerateSecret(recordHistory = true): void { let secret = '' if (this.mode() === 'password') secret = this.generatePassword() else if (this.mode() === 'passphrase') secret = this.generatePassphrase() else secret = this.generatePin() this.currentSecret.set(secret) if (recordHistory && secret) { const strength = this.currentStrength() const item: HistoryItem = { id: `hist-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, secret, mode: this.mode(), entropy: strength.entropy, strengthLabel: strength.label, createdAt: 'Just now', } this.history.update((prev) => [item, ...prev.slice(0, 7)]) } } handleManualRegenerate(): void { this.isSpinning.set(true) this.regenerateSecret(true) setTimeout(() => this.isSpinning.set(false), 400) } async copySecret(text: string, isHistory = false, historyId = ''): Promise<void> { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(text) } else { const textarea = document.createElement('textarea') textarea.value = text document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) } if (isHistory && historyId) { this.copiedHistoryId.set(historyId) setTimeout(() => { if (this.copiedHistoryId() === historyId) this.copiedHistoryId.set(null) }, 2000) } else { this.copied.set(true) setTimeout(() => this.copied.set(false), 2000) } } catch (err) { console.error('Failed to copy to clipboard', err) } } toggleHistoryReveal(id: string): void { this.revealed.update((prev) => ({ ...prev, [id]: !prev[id] })) } clearHistory(): void { this.history.set([]) } }src/app/components/blocks/password-generator-widget/index.ts0.1 kB
export { UiPasswordGeneratorWidgetComponent, type HistoryItem, type GeneratorMode, } from './password-generator-widget.component'
Raw manifest:https://uipkge.dev/r/angular/password-generator-widget.json