style(account): improve API token UX

- Replace token dismiss button with close icon in top-right corner
- Add terminal icon to token rows matching passkey layout
- Remove token row from DOM immediately on revoke
- Focus token name input when opening the create form
- Clear token name input after successful creation
This commit is contained in:
Jon Seager 2026-02-07 11:25:30 +00:00
parent 902d52b171
commit 4eed3658f2
No known key found for this signature in database
2 changed files with 31 additions and 19 deletions

View file

@ -118,7 +118,9 @@
{% else %} {% else %}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{% for token in tokens %} {% for token in tokens %}
<div class="flex items-center justify-between gap-4 rounded-md bg-surface-alt px-4 py-3"> <div id="token-row-{{ token.id }}" class="flex items-center justify-between gap-4 rounded-md bg-surface-alt px-4 py-3">
<div class="flex items-center gap-3 min-w-0">
{{ icons::terminal("h-4 w-4 text-accent shrink-0") }}
<div class="min-w-0"> <div class="min-w-0">
<span class="block text-sm font-semibold text-text">{{ token.name }}</span> <span class="block text-sm font-semibold text-text">{{ token.name }}</span>
<span class="block text-xs text-text-muted"> <span class="block text-xs text-text-muted">
@ -126,6 +128,7 @@
· {% if let Some(last_used) = token.last_used_at %}Last used {{ last_used }}{% else %}Never used{% endif %} · {% if let Some(last_used) = token.last_used_at %}Last used {{ last_used }}{% else %}Never used{% endif %}
</span> </span>
</div> </div>
</div>
<button <button
type="button" type="button"
class="shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-text-muted transition hover:text-red-600" class="shrink-0 inline-flex h-8 w-8 items-center justify-center rounded-md p-1 text-text-muted transition hover:text-red-600"
@ -147,7 +150,7 @@
<form <form
data-on:submit="$_creatingToken = true; $_tokenError = ''; @post('/api/v1/tokens', {contentType: 'form'})" data-on:submit="$_creatingToken = true; $_tokenError = ''; @post('/api/v1/tokens', {contentType: 'form'})"
data-on:datastar-fetch="if (!$_creatingToken) return; data-on:datastar-fetch="if (!$_creatingToken) return;
if (evt.detail.type === 'finished') { $_creatingToken = false } if (evt.detail.type === 'finished') { $_creatingToken = false; document.getElementById('token-name').value = '' }
else if (evt.detail.type === 'error') { $_creatingToken = false; $_tokenError = 'Failed to create token.' }" else if (evt.detail.type === 'error') { $_creatingToken = false; $_tokenError = 'Failed to create token.' }"
> >
<div class="flex items-end gap-3"> <div class="flex items-end gap-3">
@ -156,6 +159,7 @@
<input <input
type="text" type="text"
name="name" name="name"
id="token-name"
required required
class="input-field" class="input-field"
placeholder="e.g. laptop-cli, ci-server" placeholder="e.g. laptop-cli, ci-server"
@ -181,8 +185,16 @@
<!-- One-time token display --> <!-- One-time token display -->
<div data-show="$_tokenCreated" style="display: none" <div data-show="$_tokenCreated" style="display: none"
class="rounded-md border border-green-300 dark:border-green-800 bg-green-50 dark:bg-green-950/40 p-4"> class="relative rounded-md border border-green-300 dark:border-green-800 bg-green-50 dark:bg-green-950/40 p-4">
<p class="text-sm font-medium text-green-800 dark:text-green-300">Token created! Copy it now — you won't see it again.</p> <button
type="button"
onclick="window.location.reload()"
class="absolute top-3.5 right-3 inline-flex h-6 w-6 items-center justify-center rounded text-green-800 dark:text-green-300 transition hover:text-green-600 dark:hover:text-green-100"
aria-label="Dismiss"
>
{{ icons::x_mark("h-4 w-4") }}
</button>
<p class="pr-6 text-sm font-medium text-green-800 dark:text-green-300">Token created! Copy it now — you won't see it again.</p>
<div class="mt-3 flex items-center gap-2"> <div class="mt-3 flex items-center gap-2">
<code id="token-value" data-text="$_tokenValue" <code id="token-value" data-text="$_tokenValue"
class="flex-1 rounded bg-surface px-3 py-2 text-sm font-mono text-text border border-green-200 dark:border-green-800 break-all select-all"></code> class="flex-1 rounded bg-surface px-3 py-2 text-sm font-mono text-text border border-green-200 dark:border-green-800 break-all select-all"></code>
@ -194,20 +206,13 @@
Copy Copy
</button> </button>
</div> </div>
<button
type="button"
onclick="window.location.reload()"
class="mt-2 text-sm text-text-muted hover:text-text"
>
Dismiss
</button>
</div> </div>
<div> <div>
<button <button
type="button" type="button"
data-show="!$_showTokenForm && !$_tokenCreated" data-show="!$_showTokenForm && !$_tokenCreated"
data-on:click="$_showTokenForm = true" data-on:click="$_showTokenForm = true; setTimeout(() => document.getElementById('token-name').focus(), 50)"
class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover" class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-text transition hover:bg-accent-hover"
> >
Create Token Create Token
@ -439,7 +444,8 @@
try { try {
const response = await fetch(`/api/v1/tokens/${id}/revoke`, { method: "POST" }); const response = await fetch(`/api/v1/tokens/${id}/revoke`, { method: "POST" });
if (response.ok) { if (response.ok) {
window.location.reload(); const row = document.getElementById(`token-row-${id}`);
if (row) row.remove();
} else { } else {
alert("Failed to revoke token."); alert("Failed to revoke token.");
} }

View file

@ -158,6 +158,12 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro terminal(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M3.25 3A2.25 2.25 0 0 0 1 5.25v9.5A2.25 2.25 0 0 0 3.25 17h13.5A2.25 2.25 0 0 0 19 14.75v-9.5A2.25 2.25 0 0 0 16.75 3H3.25Zm.943 8.752a.75.75 0 0 1 .055-1.06L6.128 9l-1.88-1.693a.75.75 0 1 1 1.004-1.114l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.06-.055ZM9.75 10.25a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z" clip-rule="evenodd" />
</svg>
{% endmacro %}
{% macro x_mark(class) %} {% macro x_mark(class) %}
<svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="{{ class }}" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" /> <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />