From eecda919e6282c214660d02eb5a22c51a341dbe0 Mon Sep 17 00:00:00 2001 From: Vitalii Litvinchuk Date: Thu, 11 Jun 2026 20:14:31 +0300 Subject: [PATCH] refactor: improve generic typing for unlock and overhaul example app UI styling --- core/src/useCryptoLocker.ts | 11 +- example/src/App.css | 371 +++++++++++++++++++++--------------- example/src/App.tsx | 99 ++++++---- 3 files changed, 290 insertions(+), 191 deletions(-) diff --git a/core/src/useCryptoLocker.ts b/core/src/useCryptoLocker.ts index aca70e0..2afdc17 100644 --- a/core/src/useCryptoLocker.ts +++ b/core/src/useCryptoLocker.ts @@ -24,11 +24,10 @@ export function useCryptoLocker(dependencies: Record = * @param password The AES password. */ const unlock = useCallback( - async ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetchData: () => Promise, + async ( + fetchData: () => Promise, password: string - ): Promise => { + ): Promise => { setStatus("loading"); setError(null); @@ -75,9 +74,9 @@ export function useCryptoLocker(dependencies: Record = ); fn(requireFn, exportsObj, moduleObj, React); - const parsed = (moduleObj.exports.default ?? moduleObj.exports) as T; + const parsed = (moduleObj.exports.default ?? moduleObj.exports) as R; - setDecryptedData(() => parsed); + setDecryptedData(() => parsed as unknown as T); setStatus("success"); return parsed; } catch (err: unknown) { diff --git a/example/src/App.css b/example/src/App.css index 814a8e8..75932fe 100644 --- a/example/src/App.css +++ b/example/src/App.css @@ -1,173 +1,244 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); - :root { - --font: "Inter", system-ui, -apple-system, sans-serif; - --bg: #0a0a0f; - --bg-card: rgba(18, 18, 26, 0.75); - --accent-1: #6366f1; - --accent-2: #8b5cf6; - --gradient: linear-gradient(135deg, var(--accent-1), var(--accent-2)); - --text: #f0f0f5; - --text-muted: #6b7280; - --border: rgba(255, 255, 255, 0.06); - --error: #ef4444; - --success: #34d399; - --radius: 16px; - --radius-sm: 10px; - --ease: 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -*, -*::before, -*::after { - margin: 0; - padding: 0; - box-sizing: border-box; + --bg-color: #f8f9fa; + --container-bg: #ffffff; + --text-primary: #212529; + --text-secondary: #495057; + --accent-color: #0d6efd; + --accent-hover: #0b5ed7; + --error-color: #dc3545; + --error-bg: #f8d7da; + --border-color: #dee2e6; + --focus-ring: rgba(13, 110, 253, 0.5); + + --shadow-sm: 0 .125rem .25rem rgba(0,0,0,.075); + --shadow-md: 0 .5rem 1rem rgba(0,0,0,.15); + + --radius: 8px; } body { - font-family: var(--font); - background: var(--bg); - color: var(--text); - min-height: 100dvh; - -webkit-font-smoothing: antialiased; + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: var(--bg-color); + color: var(--text-primary); + min-height: 100vh; + display: flex; + flex-direction: column; + line-height: 1.5; } -#root { - min-height: 100dvh; +/* Reset focus outlines to use our custom accessible focus ring */ +*:focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 2px; } -/* Layout */ .app { - min-height: 100dvh; + width: 100%; + max-width: 600px; + padding: 2rem 1rem; + box-sizing: border-box; + margin: auto; /* Centers vertically and horizontally, allows scroll */ +} + +.glass-container { + background-color: var(--container-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 2.5rem 2rem; + box-shadow: var(--shadow-md); +} + +.lock-screen { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.lock-icon { + font-size: 3rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.lock-screen h1 { + font-size: 1.75rem; + margin: 0 0 0.5rem 0; + font-weight: 700; + color: var(--text-primary); +} + +.lock-screen p { + color: var(--text-secondary); + margin: 0 0 2rem 0; + font-size: 1rem; +} + +.input-group { + width: 100%; + max-width: 350px; + margin-bottom: 1.5rem; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.password-input { + width: 100%; + background-color: #fff; + border: 2px solid var(--border-color); + border-radius: var(--radius); + padding: 0.875rem 1rem; + color: var(--text-primary); + font-size: 1rem; + box-sizing: border-box; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + /* Monospace makes password dots clear */ + font-family: monospace; + letter-spacing: 2px; +} + +/* Accessible focus state */ +.password-input:focus { + outline: 0; + border-color: var(--accent-hover); + box-shadow: 0 0 0 0.25rem var(--focus-ring); +} + +.password-input::placeholder { + color: #6c757d; + letter-spacing: normal; + font-family: system-ui, -apple-system, sans-serif; +} + +.unlock-btn { + background-color: var(--accent-color); + color: #ffffff; + border: 2px solid transparent; + border-radius: var(--radius); + padding: 0.875rem 1.5rem; + font-size: 1.125rem; + font-weight: 600; + cursor: pointer; + width: 100%; + max-width: 350px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; +} + +.unlock-btn:hover:not(:disabled) { + background-color: var(--accent-hover); +} + +/* Ensure strong contrast for disabled state while indicating it is disabled */ +.unlock-btn:disabled { + background-color: #a5c8ff; + color: #ffffff; + cursor: not-allowed; +} + +.error-message { + background-color: var(--error-bg); + color: var(--error-color); + border: 1px solid var(--error-color); + border-radius: var(--radius); + padding: 0.75rem 1rem; + font-size: 0.875rem; + margin-top: -0.5rem; + margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: center; - padding: 24px; - position: relative; -} - -.app::before { - content: ""; - position: absolute; - width: 500px; - height: 500px; - border-radius: 50%; - background: radial-gradient(circle, rgba(99, 102, 241, 0.12), transparent 70%); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - pointer-events: none; - animation: glow 6s ease-in-out infinite; -} - -@keyframes glow { - 0%, 100% { opacity: 0.6; transform: translate(-50%, -50%) scale(1); } - 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.15); } -} - -.app__container { + gap: 0.5rem; width: 100%; - max-width: 440px; - position: relative; - z-index: 1; + max-width: 350px; + box-sizing: border-box; + font-weight: 500; } -/* CryptoLocker form overrides */ -form { +/* Unlocked State */ +.unlocked-content { display: flex; flex-direction: column; - gap: 16px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 40px 32px; - box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.5); - backdrop-filter: blur(24px); - animation: enter 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; + gap: 2rem; } -@keyframes enter { - from { opacity: 0; transform: translateY(16px) scale(0.97); } - to { opacity: 1; transform: translateY(0) scale(1); } +.unlocked-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 1rem; + border-bottom: 2px solid var(--border-color); } -form input[type="password"] { - width: 100%; - padding: 14px 18px; - font-family: var(--font); - font-size: 0.95rem; - color: var(--text); - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - outline: none; - transition: background var(--ease), border-color var(--ease), box-shadow var(--ease); -} - -form input[type="password"]::placeholder { color: var(--text-muted); } - -form input[type="password"]:focus { - background: rgba(255, 255, 255, 0.07); - border-color: rgba(99, 102, 241, 0.5); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); -} - -form p { color: var(--error); font-size: 0.85rem; text-align: center; } - -form button { - width: 100%; - padding: 14px; - font-family: var(--font); - font-size: 0.95rem; - font-weight: 600; - color: #fff; - background: var(--gradient); - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - transition: transform var(--ease), box-shadow var(--ease); - box-shadow: 0 4px 16px rgba(99, 102, 241, 0.25); -} - -form button:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: 0 8px 24px rgba(99, 102, 241, 0.35); -} - -form button:disabled { opacity: 0.5; cursor: not-allowed; } - -/* Secret content */ -.secret-content { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 40px 32px; - text-align: center; - box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.5); - backdrop-filter: blur(24px); - animation: enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.secret-content__badge { - display: inline-block; - padding: 5px 14px; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.05em; - text-transform: uppercase; - color: var(--success); - background: rgba(52, 211, 153, 0.1); - border: 1px solid rgba(52, 211, 153, 0.15); - border-radius: 999px; - margin-bottom: 16px; -} - -.secret-content__message { - font-size: 1.25rem; +.unlocked-header h2 { + margin: 0; + font-size: 1.5rem; font-weight: 700; - background: var(--gradient); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.lock-again-btn { + background-color: transparent; + color: var(--text-primary); + border: 2px solid var(--border-color); + padding: 0.5rem 1rem; + border-radius: var(--radius); + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.375rem; + transition: background-color 0.15s; +} + +.lock-again-btn:hover { + background-color: #e9ecef; +} + +.asset-preview { + background-color: var(--bg-color); + border-radius: var(--radius); + padding: 1.5rem; + border: 1px solid var(--border-color); + text-align: center; +} + +.asset-preview p { + margin: 0 0 1rem 0; + color: var(--text-primary); + font-size: 1rem; + font-weight: 600; +} + +.asset-image { + width: 100%; + max-width: 250px; + border-radius: 4px; + display: block; + margin: 0 auto 1.5rem; + border: 1px solid var(--border-color); +} + +.download-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--accent-color); + text-decoration: underline; + text-underline-offset: 4px; + font-size: 1rem; + font-weight: 600; +} + +.download-link:hover { + color: var(--accent-hover); + text-decoration-thickness: 2px; } diff --git a/example/src/App.tsx b/example/src/App.tsx index cd6e67c..2744bd4 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -53,7 +53,14 @@ export default function App() { }; if (isInitializing) { - return
Loading session...
; + return ( +
+
+
🔄
+

Initializing secure session...

+
+
+ ); } const isUnlocked = compStatus === "success" && assetStatus === "success"; @@ -62,48 +69,70 @@ export default function App() { return (
-
+
{isUnlocked && SecretComponent ? ( -
- +
+
+

+ Secure Area +

+ +
+ +
+ +
{secretImageUrl && ( -
-

Decrypted Static Asset:

- Secret - - Download Image + )} - -
) : ( -
-

Enter Password

- - setPassword(e.target.value)} - placeholder="Enter password…" - disabled={isLoading} - style={{ padding: "0.5rem", fontSize: "1rem" }} - /> - - {error &&

{error}

} - - -
+
+
🛡️
+

Encrypted Vault

+

Please enter your master password to access the secure components and assets.

+ +
+
+ setPassword(e.target.value)} + placeholder="••••••••" + disabled={isLoading} + autoFocus + /> +
+ + {error && ( +
+ ⚠️ {error} +
+ )} + + +
+
)}