Revamp login modal auth provider cards

This commit is contained in:
thePR0M3TH3AN committed 2025-10-21 18:37:41 -04:00
1 parent d390f0c444
commit 616fad1612
8 files changed
+141 -37

No files matched your search

+11 -2
View File
@@ -45,7 +45,9 @@
</p>
</div>
<section class="space-y-4 rounded-2xl border border-border bg-overlay-panel-soft p-4 sm:p-5">
<section
class="space-y-5 rounded-3xl border border-border bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
>
<div class="space-y-1">
<h3 class="text-base font-semibold text-text">Available methods</h3>
<p class="text-sm text-text-muted">
@@ -54,7 +56,10 @@
</div>
<!-- Login provider buttons are rendered dynamically -->
<div data-login-providers class="grid gap-3 sm:grid-cols-2"></div>
<div
data-login-providers
class="grid auto-rows-fr gap-4 sm:grid-cols-2 xl:grid-cols-3"
></div>
</section>
<div class="space-y-3">
@@ -83,6 +88,10 @@
>
<span class="login-option__header">
<span class="login-option__copy">
<span
data-provider-eyebrow
class="login-option__eyebrow hidden"
></span>
<span data-provider-label class="login-option__label"></span>
<span
data-provider-description
+83 -25
View File
@@ -589,76 +589,133 @@
}
.login-option {
@apply focus-ring relative flex w-full flex-col gap-3 rounded-2xl border border-border bg-overlay-panel-soft px-5 py-4 text-left text-text shadow-sm transition duration-base ease-in-out;
@apply focus-ring relative flex h-full w-full flex-col gap-4 overflow-hidden border border-border bg-overlay-panel-soft px-6 py-5 text-left text-text shadow-lg transition duration-300 ease-out;
border-radius: calc(var(--radius-xl) * 1.12);
isolation: isolate;
backdrop-filter: blur(var(--blur-modal-backdrop));
background-image: linear-gradient(
140deg,
color-mix(in srgb, var(--surface-overlay-panel-soft) 88%, transparent) 0%,
color-mix(in srgb, var(--surface-overlay-panel) 72%, transparent) 100%
);
transform: translate3d(0, 0, 0);
transition-property: border-color, box-shadow, transform;
}
.login-option:hover {
@apply border-border-strong bg-overlay-panel shadow-md;
.login-option::after {
content: "";
position: absolute;
inset: 0;
z-index: 0;
border-radius: inherit;
opacity: 0;
transform: translate3d(0, 14%, 0);
transition: opacity 220ms ease, transform 220ms ease;
background: radial-gradient(
115% 115% at 0% 0%,
color-mix(in srgb, var(--status-neutral) 32%, transparent) 0%,
transparent 65%
);
}
.login-option:hover,
.login-option:focus-visible {
@apply border-accent bg-overlay-panel shadow-md;
@apply border-border-strong;
box-shadow: var(--shadow-modal);
transform: translate3d(0, -3px, 0);
}
.login-option:hover::after,
.login-option:focus-visible::after {
opacity: 0.65;
transform: translate3d(0, 0, 0);
}
.login-option:disabled,
.login-option[aria-disabled="true"] {
@apply cursor-not-allowed opacity-60 shadow-none;
transform: none;
}
.login-option[data-tone="accent"]::after {
background: radial-gradient(
120% 120% at 0% 0%,
color-mix(in srgb, var(--status-info) 38%, transparent) 0%,
transparent 65%
);
}
.login-option[data-tone="warning"]::after {
background: radial-gradient(
120% 120% at 0% 0%,
color-mix(in srgb, var(--status-warning) 42%, transparent) 0%,
transparent 65%
);
}
.login-option--primary {
@apply shadow-sm;
@apply border-border;
}
.login-option--ghost {
@apply bg-transparent shadow-none;
}
.login-option--ghost:hover,
.login-option--ghost:focus-visible {
@apply bg-overlay-panel-soft shadow-sm;
background-image: linear-gradient(
145deg,
color-mix(in srgb, var(--surface-overlay-panel-soft) 82%, transparent) 0%,
color-mix(in srgb, var(--surface-overlay-panel-soft) 68%, transparent) 100%
);
}
.login-option--outline {
@apply bg-transparent border-border-strong;
}
.login-option--outline:hover,
.login-option--outline:focus-visible {
@apply bg-overlay-panel-soft shadow-sm;
@apply border-border-strong;
}
.login-option--link {
@apply border border-transparent bg-transparent text-accent shadow-none;
}
.login-option--link:hover,
.login-option--link:focus-visible {
@apply text-accent-strong underline;
.login-option__header,
.login-option__copy,
.login-option__status,
.login-option__capabilities {
position: relative;
z-index: 1;
}
.login-option__header {
@apply flex w-full items-start justify-between gap-3;
@apply flex w-full items-start justify-between gap-4;
}
.login-option__copy {
@apply flex min-w-0 flex-col items-start gap-1;
@apply flex min-w-0 flex-col items-start gap-2;
}
.login-option__eyebrow {
@apply text-[0.7rem] font-semibold uppercase text-text-muted;
letter-spacing: 0.18em;
}
.login-option:hover .login-option__eyebrow,
.login-option:focus-visible .login-option__eyebrow {
@apply text-text;
}
.login-option__label {
@apply text-base font-semibold text-text;
@apply text-lg font-semibold text-text;
}
.login-option__description {
@apply text-sm text-text-muted;
@apply text-sm leading-relaxed text-text-muted;
}
.login-option__chevron {
@apply inline-flex h-8 w-8 items-center justify-center rounded-full bg-panel text-text-muted transition duration-base ease-in-out;
@apply inline-flex h-9 w-9 items-center justify-center rounded-full bg-overlay-panel text-text-muted transition duration-200 ease-out;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-overlay) 60%, transparent);
}
.login-option:hover .login-option__chevron,
.login-option:focus-visible .login-option__chevron {
@apply bg-accent text-white;
box-shadow: none;
}
.login-option__status {
@@ -667,6 +724,7 @@
.login-option__capabilities {
@apply flex flex-wrap gap-2;
position: relative;
}
.btn-ghost[data-state="active"],
+2 -2
View File
@@ -59,8 +59,8 @@ if (!Object.prototype.hasOwnProperty.call(providerMap, "nip07")) {
const builtInMetadata = [
{
id: "nsec",
label: "Direct key",
description: "Use a saved nsec key stored locally.",
label: "nsec or seed (direct private key)",
description: "Use a saved private key stored locally.",
badgeVariant: "warning",
},
];
+4 -2
View File
@@ -1,7 +1,7 @@
const PROVIDER_ID = "nip07";
const PROVIDER_LABEL = "Login with Extension";
const PROVIDER_LABEL = "extension (nip-07)";
const PROVIDER_DESCRIPTION =
"Use a NIP-07 compatible browser extension to authenticate.";
"Authorize with a NIP-07 browser extension like Alby or nos2x.";
const PROVIDER_CAPABILITIES = Object.freeze([
Object.freeze({
id: "signing",
@@ -47,6 +47,8 @@ export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
eyebrow: "Recommended",
tone: "accent",
badgeVariant: "info",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
+5 -2
View File
@@ -1,6 +1,7 @@
const PROVIDER_ID = "nip46";
const PROVIDER_LABEL = "Remote signer";
const PROVIDER_DESCRIPTION = "Connect to a NIP-46 compatible signer via Nostr Connect.";
const PROVIDER_LABEL = "remote signer (nip-46)";
const PROVIDER_DESCRIPTION =
"Connect to a remote signer via Nostr Connect for hardware or mobile approvals.";
const PROVIDER_CAPABILITIES = Object.freeze([
Object.freeze({
id: "signing",
@@ -63,6 +64,8 @@ export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
eyebrow: "Nostr Connect",
tone: "accent",
badgeVariant: "info",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
+5 -2
View File
@@ -1,6 +1,7 @@
const PROVIDER_ID = "nsec";
const PROVIDER_LABEL = "Direct key";
const PROVIDER_DESCRIPTION = "Import an nsec or mnemonic seed.";
const PROVIDER_LABEL = "nsec or seed (direct private key)";
const PROVIDER_DESCRIPTION =
"Paste an nsec, hex key, or BIP-39 seed. Keep it encrypted on this device if you choose.";
const PROVIDER_CAPABILITIES = Object.freeze([
Object.freeze({
id: "signing",
@@ -64,6 +65,8 @@ export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
eyebrow: "Advanced",
tone: "warning",
badgeVariant: "warning",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
+26
View File
@@ -40,11 +40,23 @@ function normalizeProvider(provider, fallbackId, index) {
? provider.label.trim()
: rawId;
const eyebrow =
typeof provider.eyebrow === "string" && provider.eyebrow.trim()
? provider.eyebrow.trim()
: "";
const description =
typeof provider.description === "string" && provider.description.trim()
? provider.description.trim()
: "";
const rawTone =
typeof provider.tone === "string" && provider.tone.trim()
? provider.tone.trim().toLowerCase()
: "";
const tone = rawTone ? rawTone : "";
const button = provider.button && typeof provider.button === "object"
? provider.button
: {};
@@ -66,11 +78,13 @@ function normalizeProvider(provider, fallbackId, index) {
id: rawId,
source: provider,
label,
eyebrow,
description,
button,
messages,
capabilities,
order,
tone,
disabled: provider.disabled === true || provider.available === false,
errorMessage:
typeof provider.errorMessage === "string" && provider.errorMessage.trim()
@@ -949,6 +963,12 @@ export default class LoginModalController {
provider.button?.variant,
provider.button?.className,
);
if (provider.tone) {
button.dataset.tone = provider.tone;
} else {
delete button.dataset.tone;
button.removeAttribute("data-tone");
}
button.disabled = provider.disabled;
if (provider.disabled) {
button.setAttribute("aria-disabled", "true");
@@ -956,6 +976,12 @@ export default class LoginModalController {
button.removeAttribute("aria-disabled");
}
const eyebrowNode = button.querySelector("[data-provider-eyebrow]");
if (eyebrowNode instanceof HTMLElement) {
eyebrowNode.textContent = provider.eyebrow || "";
eyebrowNode.classList.toggle("hidden", !provider.eyebrow);
}
const labelNode = button.querySelector("[data-provider-label]");
if (labelNode instanceof HTMLElement) {
labelNode.textContent = provider.label;
+5 -2
View File
@@ -582,14 +582,17 @@ for (const _ of [0]) {
assert.equal(extensionButton.dataset.providerId, 'nip07');
const extensionLabel = extensionButton.querySelector('[data-provider-variant]');
assert.ok(extensionLabel, 'extension entry should include a provider badge');
assert.equal(extensionLabel.textContent, 'Login with Extension');
assert.equal(extensionLabel.textContent, 'extension (nip-07)');
assert.equal(extensionLabel.dataset.providerVariant, 'info');
assert.equal(extensionButton.getAttribute('aria-pressed'), 'false');
assert.equal(directKeyButton.dataset.providerId, 'nsec');
const directKeyLabel = directKeyButton.querySelector('[data-provider-variant]');
assert.ok(directKeyLabel, 'direct key entry should include a provider badge');
assert.equal(directKeyLabel.textContent, 'Direct key');
assert.equal(
directKeyLabel.textContent,
'nsec or seed (direct private key)',
);
assert.equal(directKeyLabel.dataset.providerVariant, 'warning');
assert.equal(directKeyButton.getAttribute('aria-pressed'), 'false');
});