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 shadcn@latest add https://uipkge.dev/r/react/password-generator-widget.json$npx shadcn@latest add https://uipkge.dev/r/react/password-generator-widget.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/password-generator-widget.json$bunx shadcn@latest add https://uipkge.dev/r/react/password-generator-widget.jsonNamed registry:
npx shadcn@latest add @uipkge-react/password-generator-widgetInstalls to:components/blocks/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: 'password' | 'passphrase' | 'pin'
entropy: number
strengthLabel: string
createdAt: string
}Files installed (1)
components/blocks/PasswordGeneratorWidget.tsx41.4 kB
'use client' import * as React from 'react' import { Check, Clock, Copy, Dice5, Eye, EyeOff, Hash, History, KeyRound, Lock, RefreshCw, Shield, ShieldCheck, Sliders, Trash2, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Slider } from '@/components/ui/slider' import { Switch } from '@/components/ui/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' // --- WORD LIST & CONSTANTS --- 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', '|', '`', "'", '"', ',', ';']) export interface HistoryItem { id: string secret: string mode: 'password' | 'passphrase' | 'pin' entropy: number strengthLabel: string createdAt: string } 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) } export function PasswordGeneratorWidget({ className }: { className?: string }) { // --- STATE --- const [mode, setMode] = React.useState<'password' | 'passphrase' | 'pin'>('password') const [passwordLength, setPasswordLength] = React.useState(20) const [includeUpper, setIncludeUpper] = React.useState(true) const [includeLower, setIncludeLower] = React.useState(true) const [includeNumbers, setIncludeNumbers] = React.useState(true) const [includeSymbols, setIncludeSymbols] = React.useState(true) const [avoidAmbiguous, setAvoidAmbiguous] = React.useState(true) // Passphrase state const [wordCount, setWordCount] = React.useState(5) const [separator, setSeparator] = React.useState('-') const [capitalizeWords, setCapitalizeWords] = React.useState(true) const [includeNumberInPassphrase, setIncludeNumberInPassphrase] = React.useState(true) // PIN state const [pinLength, setPinLength] = React.useState(6) const [avoidPinRepeats, setAvoidPinRepeats] = React.useState(true) // Output & UI state const [currentSecret, setCurrentSecret] = React.useState('') const [copied, setCopied] = React.useState(false) const [isSpinning, setIsSpinning] = React.useState(false) const [copiedHistoryId, setCopiedHistoryId] = React.useState<string | null>(null) const [revealedHistoryIds, setRevealedHistoryIds] = React.useState<Set<string>>(new Set()) // History state const [history, setHistory] = React.useState<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', }, ]) // --- ENTROPY & STRENGTH METRICS --- const currentStrength = React.useMemo(() => { let entropy = 0 if (mode === 'password') { let poolSize = 0 if (includeUpper) poolSize += avoidAmbiguous ? 24 : 26 if (includeLower) poolSize += avoidAmbiguous ? 24 : 26 if (includeNumbers) poolSize += avoidAmbiguous ? 8 : 10 if (includeSymbols) poolSize += avoidAmbiguous ? 26 : 28 entropy = poolSize > 1 ? Math.round(passwordLength * Math.log2(poolSize)) : 0 } else if (mode === 'passphrase') { entropy = wordCount * Math.log2(WORD_LIST.length) if (capitalizeWords) entropy += wordCount * 1 if (includeNumberInPassphrase) entropy += Math.log2(90) entropy = Math.round(entropy) } else { entropy = Math.round(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', } }, [ mode, passwordLength, includeUpper, includeLower, includeNumbers, includeSymbols, avoidAmbiguous, wordCount, capitalizeWords, includeNumberInPassphrase, pinLength, ]) // --- GENERATION FUNCTIONS --- const generatePassword = React.useCallback((): string => { let pool = '' const requiredChars: string[] = [] const filterPool = (chars: string) => { if (!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 (includeUpper && uppers.length > 0) { pool += uppers requiredChars.push(uppers[getRandomInt(uppers.length)]) } if (includeLower && lowers.length > 0) { pool += lowers requiredChars.push(lowers[getRandomInt(lowers.length)]) } if (includeNumbers && numbers.length > 0) { pool += numbers requiredChars.push(numbers[getRandomInt(numbers.length)]) } if (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 < passwordLength; i++) { result.push(pool[getRandomInt(pool.length)]) } // Fisher-Yates shuffle 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('') }, [avoidAmbiguous, includeLower, includeNumbers, includeSymbols, includeUpper, passwordLength]) const generatePassphrase = React.useCallback((): string => { const chosenWords: string[] = [] for (let i = 0; i < wordCount; i++) { let word = WORD_LIST[getRandomInt(WORD_LIST.length)] if (capitalizeWords) { word = word.charAt(0).toUpperCase() + word.slice(1) } chosenWords.push(word) } if (includeNumberInPassphrase) { const num = getRandomInt(90) + 10 // 2-digit number (10-99) chosenWords.push(num.toString()) } return chosenWords.join(separator) }, [capitalizeWords, includeNumberInPassphrase, separator, wordCount]) const generatePin = React.useCallback((): string => { const digits: string[] = [] for (let i = 0; i < pinLength; i++) { let nextDigit = getRandomInt(10).toString() if (avoidPinRepeats && digits.length > 0 && nextDigit === digits[digits.length - 1]) { nextDigit = ((parseInt(nextDigit, 10) + 1 + getRandomInt(8)) % 10).toString() } digits.push(nextDigit) } return digits.join('') }, [avoidPinRepeats, pinLength]) const regenerateSecret = React.useCallback( (recordHistory = false) => { let secret = '' if (mode === 'password') { secret = generatePassword() } else if (mode === 'passphrase') { secret = generatePassphrase() } else { secret = generatePin() } setCurrentSecret(secret) if (recordHistory && secret) { const ent = currentStrength.entropy const item: HistoryItem = { id: `hist-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, secret, mode, entropy: ent, strengthLabel: currentStrength.label, createdAt: 'Just now', } setHistory((prev) => [item, ...prev.slice(0, 7)]) } }, [currentStrength.entropy, currentStrength.label, generatePassphrase, generatePassword, generatePin, mode], ) const handleManualRegenerate = React.useCallback(() => { setIsSpinning(true) regenerateSecret(true) setTimeout(() => { setIsSpinning(false) }, 400) }, [regenerateSecret]) // Ensure at least one character type stays active in password mode const handleUpperChange = (val: boolean) => { if (!val && !includeLower && !includeNumbers && !includeSymbols) return setIncludeUpper(val) } const handleLowerChange = (val: boolean) => { if (!val && !includeUpper && !includeNumbers && !includeSymbols) return setIncludeLower(val) } const handleNumbersChange = (val: boolean) => { if (!val && !includeUpper && !includeLower && !includeSymbols) return setIncludeNumbers(val) } const handleSymbolsChange = (val: boolean) => { if (!val && !includeUpper && !includeLower && !includeNumbers) return setIncludeSymbols(val) } // Reactive regeneration on parameter change React.useEffect(() => { regenerateSecret(false) }, [ mode, passwordLength, includeUpper, includeLower, includeNumbers, includeSymbols, avoidAmbiguous, wordCount, separator, capitalizeWords, includeNumberInPassphrase, pinLength, avoidPinRepeats, ]) // --- CHARACTER FORMATTING FOR SYNTAX HIGHLIGHTING --- const formattedCharacters = React.useMemo(() => { if (!currentSecret) return [] return currentSecret.split('').map((char) => { let type: 'number' | 'symbol' | 'letter' | 'separator' = 'letter' if (/[0-9]/.test(char)) { type = 'number' } else if (/[-_. /]/.test(char)) { type = 'separator' } else if (/[^a-zA-Z0-9]/.test(char)) { type = 'symbol' } return { char, type } }) }, [currentSecret]) const formatItemCharacters = (text: string) => { return text.split('').map((char) => { let type: 'number' | 'symbol' | 'letter' | 'separator' = 'letter' if (/[0-9]/.test(char)) { type = 'number' } else if (/[-_. /]/.test(char)) { type = 'separator' } else if (/[^a-zA-Z0-9]/.test(char)) { type = 'symbol' } return { char, type } }) } // --- CLIPBOARD --- const copySecret = async (text: string, isHistory = false, historyId = '') => { 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) { setCopiedHistoryId(historyId) setTimeout(() => { setCopiedHistoryId((prev) => (prev === historyId ? null : prev)) }, 2000) } else { setCopied(true) setTimeout(() => { setCopied(false) }, 2000) } } catch (err) { console.error('Failed to copy to clipboard', err) } } const toggleHistoryReveal = (id: string) => { setRevealedHistoryIds((prev) => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) } const clearHistory = () => { setHistory([]) } return ( <div data-slot="password-generator-widget" className={cn('mx-auto w-full max-w-4xl space-y-6', className)}> {/* HERO: GENERATOR OUTPUT CARD */} <Card className="border-border overflow-hidden shadow-xs"> <CardHeader className="border-border/50 bg-muted/20 border-b pb-3"> <div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex items-center gap-2"> <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <KeyRound className="size-4" /> </div> <div> <CardTitle className="text-base font-semibold">Generated Secret</CardTitle> <CardDescription className="text-xs"> Client-side cryptographic output & entropy estimation </CardDescription> </div> </div> <div className="flex items-center gap-2"> <Badge variant="outline" className="gap-1.5 text-xs font-normal"> <span className="bg-success size-2 animate-pulse rounded-full" /> 100% Client-Side </Badge> </div> </div> </CardHeader> <CardContent className="space-y-6 pt-6"> {/* Monospace Output Box */} <div className="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 className="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"> {formattedCharacters.length > 0 ? ( formattedCharacters.map((item, idx) => ( <span key={idx} className={cn( item.type === 'number' && 'text-info font-semibold', item.type === 'symbol' && 'text-warning font-semibold', item.type === 'separator' && 'text-muted-foreground px-0.5 font-normal', item.type === 'letter' && 'text-foreground font-medium', )} > {item.char} </span> )) ) : ( <span className="text-muted-foreground text-sm">Generating secret...</span> )} </div> {/* Quick Action Buttons */} <div className="flex shrink-0 items-center gap-2 self-end sm:self-center"> <Button variant="outline" size="sm" className="h-9 cursor-pointer gap-1.5 px-3 active:scale-95" onClick={handleManualRegenerate} > <RefreshCw className={cn('size-4', isSpinning && 'text-primary animate-spin')} /> <span className="text-xs font-medium">Regenerate</span> </Button> <Button variant="default" size="sm" className={cn( 'h-9 cursor-pointer gap-1.5 px-3.5 active:scale-95', copied && 'bg-success hover:bg-success text-white', )} onClick={() => copySecret(currentSecret)} > {copied ? <Check className="size-4 stroke-[2.5]" /> : <Copy className="size-4" />} <span className="text-xs font-medium">{copied ? 'Copied!' : 'Copy'}</span> </Button> </div> </div> {/* Entropy & Strength Meter */} <div className="border-border/60 bg-muted/10 space-y-2.5 rounded-lg border p-4"> <div className="flex flex-wrap items-center justify-between gap-2 text-xs"> <div className="flex items-center gap-2"> <ShieldCheck className={cn('size-4', currentStrength.color)} /> <span className="text-foreground font-semibold">{currentStrength.label}</span> <span className="text-muted-foreground font-mono">{currentStrength.entropy}-bit entropy</span> </div> <div className="text-muted-foreground flex items-center gap-1.5"> <Clock className="size-3.5" /> <span>Crack time:</span> <span className="text-foreground font-medium">{currentStrength.crackTime}</span> </div> </div> {/* 4-Segment Strength Bar */} <div className="grid h-2 w-full grid-cols-4 gap-1.5 overflow-hidden rounded-full"> <div className={cn( 'rounded-full transition-colors duration-300', currentStrength.score >= 1 ? currentStrength.barClass : 'bg-muted/80 dark:bg-muted', )} /> <div className={cn( 'rounded-full transition-colors duration-300', currentStrength.score >= 2 ? currentStrength.barClass : 'bg-muted/80 dark:bg-muted', )} /> <div className={cn( 'rounded-full transition-colors duration-300', currentStrength.score >= 3 ? currentStrength.barClass : 'bg-muted/80 dark:bg-muted', )} /> <div className={cn( 'rounded-full transition-colors duration-300', currentStrength.score >= 4 ? currentStrength.barClass : 'bg-muted/80 dark:bg-muted', )} /> </div> {/* Monospace Character Type Legend */} <div className="text-muted-foreground flex flex-wrap items-center gap-4 pt-1 text-xs"> <span className="flex items-center gap-1.5"> <span className="bg-foreground size-2 rounded-full" /> Letters </span> <span className="flex items-center gap-1.5"> <span className="bg-info size-2 rounded-full" /> Numbers (0-9) </span> <span className="flex items-center gap-1.5"> <span className="bg-warning size-2 rounded-full" /> Symbols (!@#$) </span> </div> </div> </CardContent> </Card> {/* GENERATOR CONFIGURATION CARD */} <Card className="border-border shadow-xs"> <CardHeader className="border-border/50 border-b pb-3"> <div className="flex items-center gap-2"> <div className="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg"> <Sliders className="size-4" /> </div> <div> <CardTitle className="text-base font-semibold">Generator Configuration</CardTitle> <CardDescription className="text-xs"> Customize character pools, length constraints, and format patterns </CardDescription> </div> </div> </CardHeader> <CardContent className="pt-6"> <Tabs value={mode} onValueChange={(val) => setMode(val as 'password' | 'passphrase' | 'pin')} className="w-full" > <TabsList className="mb-6 grid w-full grid-cols-3"> <TabsTrigger value="password" className="gap-1.5 text-xs"> <Lock className="size-3.5" /> <span>Random Password</span> </TabsTrigger> <TabsTrigger value="passphrase" className="gap-1.5 text-xs"> <Dice5 className="size-3.5" /> <span>Passphrase</span> </TabsTrigger> <TabsTrigger value="pin" className="gap-1.5 text-xs"> <Hash className="size-3.5" /> <span>PIN Code</span> </TabsTrigger> </TabsList> {/* TAB 1: RANDOM PASSWORD */} <TabsContent value="password" className="mt-0 space-y-6"> {/* Length Control */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-medium">Password Length</label> <Badge variant="secondary" className="font-mono text-xs font-semibold tabular-nums"> {passwordLength} characters </Badge> </div> <Slider value={[passwordLength]} min={8} max={64} step={1} className="w-full py-1" onValueChange={([val]) => setPasswordLength(val)} /> {/* Preset length buttons */} <div className="flex flex-wrap items-center gap-1.5 pt-1"> <span className="text-muted-foreground mr-1 text-xs">Presets:</span> {[12, 16, 20, 24, 32, 48, 64].map((preset) => ( <button key={preset} type="button" className={cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', passwordLength === preset ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', )} onClick={() => setPasswordLength(preset)} > {preset} </button> ))} </div> </div> <Separator /> {/* Character Type Switches */} <div className="space-y-3"> <label className="text-muted-foreground text-xs font-medium">Character Pools</label> <div className="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> {/* Uppercase */} <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Uppercase Letters</span> <span className="text-muted-foreground font-mono text-xs font-normal">(A-Z)</span> </div> <p className="text-muted-foreground text-xs">Include capital Latin characters</p> </div> <Switch checked={includeUpper} onCheckedChange={handleUpperChange} /> </div> <Separator /> {/* Lowercase */} <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Lowercase Letters</span> <span className="text-muted-foreground font-mono text-xs font-normal">(a-z)</span> </div> <p className="text-muted-foreground text-xs">Include small Latin characters</p> </div> <Switch checked={includeLower} onCheckedChange={handleLowerChange} /> </div> <Separator /> {/* Numbers */} <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Numbers</span> <span className="text-info font-mono text-xs font-semibold">(0-9)</span> </div> <p className="text-muted-foreground text-xs">Include numeric digits 0 through 9</p> </div> <Switch checked={includeNumbers} onCheckedChange={handleNumbersChange} /> </div> <Separator /> {/* Symbols */} <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Special Symbols</span> <span className="text-warning font-mono text-xs font-semibold">(!@#$%^&*)</span> </div> <p className="text-muted-foreground text-xs">Include non-alphanumeric punctuation marks</p> </div> <Switch checked={includeSymbols} onCheckedChange={handleSymbolsChange} /> </div> <Separator /> {/* Avoid Ambiguous */} <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground flex items-center gap-2 text-sm font-medium"> <span>Avoid Ambiguous Characters</span> <span className="text-muted-foreground font-mono text-xs font-normal">(l, 1, I, O, 0)</span> </div> <p className="text-muted-foreground text-xs">Exclude confusing lookalike letters and digits</p> </div> <Switch checked={avoidAmbiguous} onCheckedChange={setAvoidAmbiguous} /> </div> </div> </div> </TabsContent> {/* TAB 2: MEMORABLE PASSPHRASE */} <TabsContent value="passphrase" className="mt-0 space-y-6"> {/* Word Count Control */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-medium">Number of Words</label> <Badge variant="secondary" className="font-mono text-xs font-semibold tabular-nums"> {wordCount} words </Badge> </div> <Slider value={[wordCount]} min={3} max={8} step={1} className="w-full py-1" onValueChange={([val]) => setWordCount(val)} /> {/* Preset word count buttons */} <div className="flex flex-wrap items-center gap-1.5 pt-1"> <span className="text-muted-foreground mr-1 text-xs">Presets:</span> {[3, 4, 5, 6, 8].map((preset) => ( <button key={preset} type="button" className={cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', wordCount === preset ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', )} onClick={() => setWordCount(preset)} > {preset} words </button> ))} </div> </div> <Separator /> {/* Separator Selector */} <div className="space-y-3"> <label className="text-muted-foreground text-xs font-medium">Word Separator</label> <div className="grid grid-cols-5 gap-2"> {[ { id: '-', label: 'Hyphen (-)' }, { id: '_', label: 'Under (_)' }, { id: '.', label: 'Period (.)' }, { id: ' ', label: 'Space ( )' }, { id: '/', label: 'Slash (/)' }, ].map((sep) => ( <button key={sep.id} type="button" className={cn( 'flex cursor-pointer flex-col items-center justify-center rounded-lg border p-2.5 text-xs font-medium transition-colors', separator === sep.id ? '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', )} onClick={() => setSeparator(sep.id)} > <span className="font-mono text-sm font-semibold">{sep.id === ' ' ? '␣' : sep.id}</span> <span className="mt-0.5 truncate text-xs">{sep.label.split(' ')[0]}</span> </button> ))} </div> </div> <Separator /> {/* Passphrase Switches */} <div className="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground text-sm font-medium">Capitalize Words</div> <p className="text-muted-foreground text-xs">Transform each word into TitleCase for easy reading</p> </div> <Switch checked={capitalizeWords} onCheckedChange={setCapitalizeWords} /> </div> <Separator /> <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground text-sm font-medium">Append Random Number</div> <p className="text-muted-foreground text-xs"> Add a 2-digit number (10-99) for additional security </p> </div> <Switch checked={includeNumberInPassphrase} onCheckedChange={setIncludeNumberInPassphrase} /> </div> </div> </TabsContent> {/* TAB 3: PIN CODE */} <TabsContent value="pin" className="mt-0 space-y-6"> {/* Length Control */} <div className="space-y-3"> <div className="flex items-center justify-between"> <label className="text-foreground text-sm font-medium">PIN Length</label> <Badge variant="secondary" className="font-mono text-xs font-semibold tabular-nums"> {pinLength} digits </Badge> </div> <Slider value={[pinLength]} min={4} max={16} step={1} className="w-full py-1" onValueChange={([val]) => setPinLength(val)} /> {/* Preset PIN length buttons */} <div className="flex flex-wrap items-center gap-1.5 pt-1"> <span className="text-muted-foreground mr-1 text-xs">Presets:</span> {[4, 6, 8, 10, 12, 16].map((preset) => ( <button key={preset} type="button" className={cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', pinLength === preset ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', )} onClick={() => setPinLength(preset)} > {preset} digits </button> ))} </div> </div> <Separator /> {/* PIN Rules */} <div className="border-border/80 bg-muted/20 space-y-3 rounded-lg border p-4"> <div className="flex items-center justify-between"> <div className="space-y-0.5"> <div className="text-foreground text-sm font-medium">Avoid Consecutive Repeated Digits</div> <p className="text-muted-foreground text-xs"> Prevent obvious repetitive patterns like “1122” or “8888” </p> </div> <Switch checked={avoidPinRepeats} onCheckedChange={setAvoidPinRepeats} /> </div> </div> </TabsContent> </Tabs> </CardContent> </Card> {/* RECENT HISTORY CARD */} <Card className="border-border shadow-xs"> <CardHeader className="border-border/50 border-b pb-3"> <div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex items-center gap-2"> <div className="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-lg"> <History className="size-4" /> </div> <div> <CardTitle className="text-base font-semibold">Generated History</CardTitle> <CardDescription className="text-xs"> Stored in volatile browser memory only • Cleared on page refresh </CardDescription> </div> </div> {history.length > 0 && ( <Button variant="ghost" size="sm" className="text-muted-foreground hover:text-destructive h-8 cursor-pointer gap-1.5 text-xs" onClick={clearHistory} > <Trash2 className="size-3.5" /> <span>Clear History</span> </Button> )} </div> </CardHeader> <CardContent className="pt-4"> {history.length === 0 ? ( <div className="text-muted-foreground py-8 text-center text-xs"> No recent secrets in session history. Regenerate or copy above to log items here. </div> ) : ( <div className="divide-border/60 divide-y"> {history.map((item) => ( <div key={item.id} className="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 className="flex min-w-0 items-center gap-3"> <div className="bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-md"> {item.mode === 'password' && <Lock className="size-3.5" />} {item.mode === 'passphrase' && <Dice5 className="size-3.5" />} {item.mode === 'pin' && <Hash className="size-3.5" />} </div> <div className="min-w-0 space-y-1"> <div className="flex items-center gap-2"> {/* Monospace secret or masked bullets */} <div className="max-w-[18rem] truncate font-mono text-xs font-medium md:max-w-md"> {revealedHistoryIds.has(item.id) ? ( formatItemCharacters(item.secret).map((subChar, idx) => ( <span key={idx} className={cn( subChar.type === 'number' && 'text-info font-semibold', subChar.type === 'symbol' && 'text-warning font-semibold', subChar.type === 'separator' && 'text-muted-foreground font-normal', subChar.type === 'letter' && 'text-foreground', )} > {subChar.char} </span> )) ) : ( <span className="text-muted-foreground tracking-widest"> •••••••••••••••• </span> )} </div> {/* Reveal Toggle */} <button type="button" className="text-muted-foreground hover:text-foreground cursor-pointer p-0.5 transition-colors" title="Toggle Visibility" onClick={() => toggleHistoryReveal(item.id)} > {revealedHistoryIds.has(item.id) ? ( <EyeOff className="size-3.5" /> ) : ( <Eye className="size-3.5" /> )} </button> </div> <div className="text-muted-foreground flex items-center gap-2 text-xs"> <Badge variant="outline" className="px-1.5 py-0 text-xs font-normal"> {item.mode} </Badge> <span>•</span> <span> {item.entropy}-bit ({item.strengthLabel}) </span> <span>•</span> <span>{item.createdAt}</span> </div> </div> </div> <div className="flex shrink-0 items-center gap-2 self-end sm:self-center"> <Button variant="outline" size="sm" className={cn( 'h-8 cursor-pointer gap-1.5 px-2.5 text-xs active:scale-95', copiedHistoryId === item.id && 'bg-success hover:bg-success text-white', )} onClick={() => copySecret(item.secret, true, item.id)} > {copiedHistoryId === item.id ? <Check className="size-3.5" /> : <Copy className="size-3.5" />} <span>{copiedHistoryId === item.id ? 'Copied' : 'Copy'}</span> </Button> </div> </div> ))} </div> )} </CardContent> <CardFooter className="text-muted-foreground border-border/40 flex items-center justify-between border-t pt-0 pt-3 pb-4 text-xs"> <span className="flex items-center gap-1.5"> <Shield className="text-success size-3.5" /> Cryptographically random • Never transmitted over network </span> <span className="font-mono tabular-nums">{history.length} entries</span> </CardFooter> </Card> </div> ) }
Raw manifest:https://uipkge.dev/r/react/password-generator-widget.json