Profile view UX improvements. More consistent styling across the application.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<html style="background: transparent"><meta charset="UTF-8">
|
<!DOCTYPE html>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<html lang="en" style="background: transparent">
|
||||||
<div id="app"></div>
|
<head>
|
||||||
<script type="module" src="/auth/restricted/main.js"></script>
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/auth/restricted/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
--layout-max-width: 1400px;
|
--layout-max-width: 1400px;
|
||||||
--layout-padding: clamp(1.5rem, 3vw + 1rem, 3.25rem);
|
--layout-padding: clamp(1.5rem, 3vw + 1rem, 3.25rem);
|
||||||
--transition-base: 160ms ease;
|
--transition-base: 160ms ease;
|
||||||
|
--focus-ring: 0 0 0 2px var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -84,6 +85,10 @@ body {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-align: justify;
|
||||||
|
hyphens: auto;
|
||||||
|
-webkit-hyphens: auto;
|
||||||
|
-moz-hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
@@ -108,13 +113,6 @@ a:visited {
|
|||||||
a:hover,
|
a:hover,
|
||||||
a:focus-visible {
|
a:focus-visible {
|
||||||
color: var(--color-link-hover);
|
color: var(--color-link-hover);
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:focus-visible {
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
@@ -187,6 +185,11 @@ a:focus-visible {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -214,7 +217,6 @@ button {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.65rem 1.1rem;
|
padding: 0.65rem 1.1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-base);
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -223,11 +225,6 @@ button {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 2px solid var(--color-accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
filter: opacity(0.6);
|
filter: opacity(0.6);
|
||||||
@@ -437,7 +434,7 @@ th {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root { --card-width: 22rem; }
|
:root { --card-width: 16rem; }
|
||||||
|
|
||||||
.record-list,
|
.record-list,
|
||||||
.credential-list,
|
.credential-list,
|
||||||
@@ -447,7 +444,7 @@ th {
|
|||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr));
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
gap: 1rem 1.25rem;
|
gap: 0.5rem;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@@ -461,9 +458,7 @@ th {
|
|||||||
.session-item {
|
.session-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
padding: 0.5rem;
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -484,7 +479,7 @@ th {
|
|||||||
.credential-item.current-session,
|
.credential-item.current-session,
|
||||||
.credential-item.is-hovered,
|
.credential-item.is-hovered,
|
||||||
.session-item.is-current,
|
.session-item.is-current,
|
||||||
.session-item.is-hovered { border-color: var(--color-accent); }
|
.session-item.is-hovered { border-color: var(--color-accent); background-color: var(--color-surface-subtle); }
|
||||||
|
|
||||||
.credential-item.is-linked-session,
|
.credential-item.is-linked-session,
|
||||||
.session-item.is-linked-credential { border-color: var(--color-accent); background-color: var(--color-surface-subtle); }
|
.session-item.is-linked-credential { border-color: var(--color-accent); background-color: var(--color-surface-subtle); }
|
||||||
@@ -492,7 +487,7 @@ th {
|
|||||||
.item-top {
|
.item-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-icon {
|
.item-icon {
|
||||||
@@ -500,9 +495,6 @@ th {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--color-surface-subtle, transparent);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,12 +508,14 @@ th {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-heading);
|
color: var(--color-heading);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-actions {
|
.item-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,39 +523,30 @@ th {
|
|||||||
.item-actions .badge + .badge { margin-left: 0.25rem; }
|
.item-actions .badge + .badge { margin-left: 0.25rem; }
|
||||||
|
|
||||||
.item-details {
|
.item-details {
|
||||||
margin-left: calc(40px + 1rem);
|
margin-left: 40px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.credential-dates {
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: row;
|
|
||||||
grid-template-columns: 7rem 1fr;
|
|
||||||
gap: 0.35rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credential-dates,
|
||||||
.session-dates {
|
.session-dates {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-columns: 7rem 1fr;
|
grid-template-columns: 8em 1fr;
|
||||||
gap: 0.35rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--color-text-muted);
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-label {
|
.date-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: inherit;
|
color: var(--color-text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-value {
|
.date-value {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-card-delete { background: transparent; border: none; color: var(--color-danger); padding: 0.35rem 0.5rem; font-size: 1.05rem; line-height: 1; border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
.btn-card-delete { background: transparent; border: none; color: var(--color-danger); padding: 0.35rem 0.5rem; font-size: 1.05rem; line-height: 1; border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
@@ -578,6 +563,7 @@ th {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-current {
|
.badge-current {
|
||||||
@@ -610,28 +596,18 @@ th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
background: var(--color-surface);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 1.1rem 1.25rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
border-radius: var(--radius-md);
|
||||||
gap: 0.75rem 1.25rem;
|
background: var(--color-surface);
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info h3 {
|
.user-details {
|
||||||
margin: 0;
|
display: grid;
|
||||||
grid-column: span 2;
|
grid-template-columns: 7em 1fr;
|
||||||
display: flex;
|
gap: 0 0.5rem;
|
||||||
align-items: center;
|
padding-left: 40px;
|
||||||
gap: 0.5rem;
|
font-size: 0.75rem;
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info span {
|
|
||||||
text-align: left;
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-link {
|
.toggle-link {
|
||||||
@@ -653,11 +629,6 @@ th {
|
|||||||
gap: 1.75rem;
|
gap: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credential-dates {
|
|
||||||
grid-auto-flow: row;
|
|
||||||
grid-template-columns: auto auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-status {
|
.global-status {
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -738,3 +709,27 @@ body:has(#auth-iframe) {
|
|||||||
backdrop-filter: blur(.1rem) brightness(0.7);
|
backdrop-filter: blur(.1rem) brightness(0.7);
|
||||||
-webkit-backdrop-filter: blur(.1rem) brightness(0.7);
|
-webkit-backdrop-filter: blur(.1rem) brightness(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slot-machine {
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
background: var(--color-surface-hover, rgba(0, 0, 0, 0.03));
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-reel {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 1.8em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,5 @@ const crumbs = computed(() => {
|
|||||||
.breadcrumbs ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .25rem; }
|
.breadcrumbs ol { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: .25rem; }
|
||||||
.breadcrumbs li { display: inline-flex; align-items: center; gap: .25rem; font-size: .9rem; }
|
.breadcrumbs li { display: inline-flex; align-items: center; gap: .25rem; font-size: .9rem; }
|
||||||
.breadcrumbs a { text-decoration: none; color: var(--color-link); padding: 0 .25rem; border-radius:4px; transition: color 0.2s ease, background 0.2s ease; }
|
.breadcrumbs a { text-decoration: none; color: var(--color-link); padding: 0 .25rem; border-radius:4px; transition: color 0.2s ease, background 0.2s ease; }
|
||||||
.breadcrumbs a:hover, .breadcrumbs a:focus-visible { text-decoration: underline; color: var(--color-link-hover); outline: none; }
|
|
||||||
.breadcrumbs .sep { color: var(--color-text-muted); margin: 0; }
|
.breadcrumbs .sep { color: var(--color-text-muted); margin: 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
'is-linked-session': hoveredSessionCredentialUuid === credential.credential_uuid
|
'is-linked-session': hoveredSessionCredentialUuid === credential.credential_uuid
|
||||||
}]"
|
}]"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@mousedown.prevent
|
||||||
|
@click.capture="handleCardClick"
|
||||||
@focusin="handleCredentialFocus(credential.credential_uuid)"
|
@focusin="handleCredentialFocus(credential.credential_uuid)"
|
||||||
@focusout="handleCredentialBlur($event)"
|
@focusout="handleCredentialBlur($event)"
|
||||||
@keydown="handleDelete($event, credential)"
|
@keydown="handleDelete($event, credential)"
|
||||||
@@ -59,7 +61,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { formatDate } from '@/utils/helpers'
|
import { formatDate } from '@/utils/helpers'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -84,6 +85,13 @@ const handleCredentialBlur = (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCardClick = (event) => {
|
||||||
|
if (!event.currentTarget.matches(':focus')) {
|
||||||
|
event.currentTarget.focus()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = (event, credential) => {
|
const handleDelete = (event, credential) => {
|
||||||
const apple = navigator.userAgent.includes('Mac OS')
|
const apple = navigator.userAgent.includes('Mac OS')
|
||||||
if (event.key === 'Delete' || apple && event.key === 'Backspace') {
|
if (event.key === 'Delete' || apple && event.key === 'Backspace') {
|
||||||
@@ -105,3 +113,12 @@ const getCredentialAuthIcon = (credential) => {
|
|||||||
return info[iconKey] || null
|
return info[iconKey] || null
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-card-delete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.credential-item:focus .btn-card-delete {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -19,24 +19,25 @@
|
|||||||
@edit-name="openNameDialog"
|
@edit-name="openNameDialog"
|
||||||
>
|
>
|
||||||
<div class="remote-auth-inline">
|
<div class="remote-auth-inline">
|
||||||
<label v-if="!showDeviceInfo" class="remote-auth-label">Code words from remote device:</label>
|
<label v-if="!showDeviceInfo" class="remote-auth-label">Code words:</label>
|
||||||
<RemoteAuth
|
<RemoteAuthPermit
|
||||||
ref="pairingEntry"
|
ref="pairingEntry"
|
||||||
title=""
|
title=""
|
||||||
description=""
|
description=""
|
||||||
placeholder="word word word"
|
|
||||||
@completed="handlePairingCompleted"
|
@completed="handlePairingCompleted"
|
||||||
@error="handlePairingError"
|
@error="handlePairingError"
|
||||||
@device-info-visible="showDeviceInfo = $event"
|
@device-info-visible="showDeviceInfo = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="remote-auth-description">Provided by another device requesting remote auth.</p>
|
||||||
</UserBasicInfo>
|
</UserBasicInfo>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section-block">
|
<section class="section-block">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Your Passkeys</h2>
|
<h2>Your Passkeys</h2>
|
||||||
<p class="section-description">Keep at least one trusted passkey so you can always sign in.</p>
|
<p class="section-description">Ideally have at least two passkeys in case you lose one. More than one user can be registered on the same device, giving you a choice at login.</p>
|
||||||
|
<p><a href="https://bitwarden.com/pricing/" target="_blank" rel="noopener noreferrer">Bitwarden</a> can sync one passkey to all your devices. Other secure options include <b>local passkeys</b>, as well as hardware keys such as <a href="https://www.yubico.com" target="_blank" rel="noopener noreferrer">YubiKey</a>. Cloud sync via Google, Microsoft or iCloud is discouraged.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<CredentialList
|
<CredentialList
|
||||||
@@ -50,8 +51,8 @@
|
|||||||
@credential-hover="hoveredCredentialUuid = $event"
|
@credential-hover="hoveredCredentialUuid = $event"
|
||||||
/>
|
/>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button @click="addNewCredential" class="btn-primary">Add New Passkey</button>
|
<button @click="addNewCredential" class="btn-primary">Register New</button>
|
||||||
<button @click="showRegLink = true" class="btn-secondary">Add Another Device</button>
|
<button @click="showRegLink = true" class="btn-secondary">Another Device</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
:hovered-credential-uuid="hoveredCredentialUuid"
|
:hovered-credential-uuid="hoveredCredentialUuid"
|
||||||
@terminate="terminateSession"
|
@terminate="terminateSession"
|
||||||
@session-hover="hoveredSession = $event"
|
@session-hover="hoveredSession = $event"
|
||||||
section-description="Review where you're signed in and end any sessions you no longer recognize."
|
section-description="You are currently signed in to the following sessions. If you don't recognize something, consider deleting not only the session but the associated passkey you suspect is compromised, as only this terminates all linked sessions and prevents logging in again."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal v-if="showNameDialog" @close="showNameDialog = false">
|
<Modal v-if="showNameDialog" @close="showNameDialog = false">
|
||||||
@@ -112,7 +113,7 @@ import Modal from '@/components/Modal.vue'
|
|||||||
import NameEditForm from '@/components/NameEditForm.vue'
|
import NameEditForm from '@/components/NameEditForm.vue'
|
||||||
import SessionList from '@/components/SessionList.vue'
|
import SessionList from '@/components/SessionList.vue'
|
||||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||||
import RemoteAuth from '@/components/RemoteAuthPermit.vue'
|
import RemoteAuthPermit from '@/components/RemoteAuthPermit.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
||||||
import passkey from '@/utils/passkey'
|
import passkey from '@/utils/passkey'
|
||||||
@@ -167,10 +168,9 @@ const handlePairingError = (message) => {
|
|||||||
const handleDelete = async (credential) => {
|
const handleDelete = async (credential) => {
|
||||||
const credentialId = credential?.credential_uuid
|
const credentialId = credential?.credential_uuid
|
||||||
if (!credentialId) return
|
if (!credentialId) return
|
||||||
if (!confirm('Are you sure you want to delete this passkey?')) return
|
|
||||||
try {
|
try {
|
||||||
await authStore.deleteCredential(credentialId)
|
await authStore.deleteCredential(credentialId)
|
||||||
authStore.showMessage('Passkey deleted successfully!', 'success', 3000)
|
authStore.showMessage('Passkey deleted! You should also remove it from your password manager or device.', 'success', 3000)
|
||||||
} catch (error) { authStore.showMessage(`Failed to delete passkey: ${error.message}`, 'error') }
|
} catch (error) { authStore.showMessage(`Failed to delete passkey: ${error.message}`, 'error') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,9 +219,12 @@ const saveName = async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.view-lede { margin: 0; color: var(--color-text-muted); font-size: 1rem; }
|
.view-lede { margin: 0; color: var(--color-text-muted); font-size: 1rem; }
|
||||||
.section-header { display: flex; flex-direction: column; gap: 0.4rem; }
|
.section-header { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||||
.section-description { margin: 0; color: var(--color-text-muted); }
|
|
||||||
.empty-state { margin: 0; color: var(--color-text-muted); text-align: center; padding: 1rem 0; }
|
.empty-state { margin: 0; color: var(--color-text-muted); text-align: center; padding: 1rem 0; }
|
||||||
.logout-note { margin: 0.75rem 0 0; color: var(--color-text-muted); font-size: 0.875rem; }
|
.logout-note { margin: 0.75rem 0 0; color: var(--color-text-muted); font-size: 0.875rem; }
|
||||||
.remote-auth-inline { display: flex; flex-direction: column; gap: 0.5rem; }
|
.remote-auth-inline { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||||
.remote-auth-label { display: block; margin: 0; font-size: 0.875rem; color: var(--color-text-muted); font-weight: 500; }
|
.remote-auth-label { display: block; margin: 0; font-size: 0.875rem; color: var(--color-text-muted); font-weight: 500; }
|
||||||
|
.remote-auth-description {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const props = defineProps({
|
|||||||
title: { type: String, default: 'Help Another Device Sign In' },
|
title: { type: String, default: 'Help Another Device Sign In' },
|
||||||
description: { type: String, default: 'Enter the code shown on the device that needs to sign in.' },
|
description: { type: String, default: 'Enter the code shown on the device that needs to sign in.' },
|
||||||
placeholder: { type: String, default: 'Enter three words' },
|
placeholder: { type: String, default: 'Enter three words' },
|
||||||
action: { type: String, default: 'login' } // 'login' or 'register'
|
action: { type: String, default: 'login' }, // 'login' or 'register'
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['completed', 'error', 'cancelled', 'back', 'register', 'deviceInfoVisible'])
|
const emit = defineEmits(['completed', 'error', 'cancelled', 'back', 'register', 'deviceInfoVisible'])
|
||||||
@@ -654,7 +654,6 @@ function reset() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchSettings()
|
await fetchSettings()
|
||||||
inputRef.value?.focus()
|
|
||||||
// Initialize cursor position
|
// Initialize cursor position
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
cursorPos.value = inputRef.value?.selectionStart ?? 0
|
cursorPos.value = inputRef.value?.selectionStart ?? 0
|
||||||
@@ -696,25 +695,14 @@ defineExpose({ reset, deny, code, handleInput, loading, error })
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slot machine visual display (matches RemoteAuthInline) */
|
/* Slot machine visual display (matches RemoteAuthRequest) */
|
||||||
.slot-machine {
|
.slot-machine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
background: var(--color-surface-hover, rgba(0, 0, 0, 0.03));
|
|
||||||
border: 2px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-sm, 6px);
|
|
||||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
gap: 0;
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -729,15 +717,8 @@ defineExpose({ reset, deny, code, handleInput, loading, error })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slot-reel {
|
.slot-reel {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1 1 33.333%;
|
flex: 1 1 33.333%;
|
||||||
min-width: 0;
|
|
||||||
height: 1.8em;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
position: relative;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-reel:not(:last-child) {
|
.slot-reel:not(:last-child) {
|
||||||
|
|||||||
@@ -360,16 +360,8 @@ defineExpose({ retry, cancel })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slot-reel {
|
.slot-reel {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 1.8em;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
background: var(--color-surface, rgba(255, 255, 255, 0.5));
|
background: var(--color-surface, rgba(255, 255, 255, 0.5));
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-machine:not(.stopped) .slot-reel:nth-child(1) {
|
.slot-machine:not(.stopped) .slot-reel:nth-child(1) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- Remote authentication view (request new remote auth) -->
|
<!-- Remote authentication view (request new remote auth) -->
|
||||||
<div v-else-if="authView === 'remote'" class="auth-view">
|
<div v-else-if="authView === 'remote'" class="auth-view">
|
||||||
<RemoteAuthInline
|
<RemoteAuthRequest
|
||||||
:active="authView === 'remote'"
|
:active="authView === 'remote'"
|
||||||
@authenticated="handleRemoteAuthenticated"
|
@authenticated="handleRemoteAuthenticated"
|
||||||
@register="handleRemoteRegistration"
|
@register="handleRemoteRegistration"
|
||||||
@@ -59,7 +59,7 @@ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|||||||
import passkey from '@/utils/passkey'
|
import passkey from '@/utils/passkey'
|
||||||
import { getSettings, uiBasePath } from '@/utils/settings'
|
import { getSettings, uiBasePath } from '@/utils/settings'
|
||||||
import { fetchJson, getUserFriendlyErrorMessage } from '@/utils/api'
|
import { fetchJson, getUserFriendlyErrorMessage } from '@/utils/api'
|
||||||
import RemoteAuthInline from '@/components/RemoteAuthRequest.vue'
|
import RemoteAuthRequest from '@/components/RemoteAuthRequest.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="section-block" data-component="session-list-section">
|
<section class="section-block" data-component="session-list-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Active Sessions</h2>
|
<h2>Login Sessions</h2>
|
||||||
<p class="section-description">{{ sectionDescription }}</p>
|
<p class="section-description">{{ sectionDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div :class="['session-list']">
|
<div>
|
||||||
<template v-if="Array.isArray(sessions) && sessions.length">
|
<template v-if="Array.isArray(sessions) && sessions.length">
|
||||||
<div v-for="(group, host) in groupedSessions" :key="host" class="session-group">
|
<div v-for="(group, host) in groupedSessions" :key="host" class="session-group">
|
||||||
<h3 :class="['session-group-host', { 'is-current-site': group.isCurrentSite }]">
|
<h3 :class="['session-group-host', { 'is-current-site': group.isCurrentSite }]">
|
||||||
<template v-if="host"><a :href="hostUrl(host)">🌐 {{ host }}</a></template>
|
<template v-if="host"><a :href="hostUrl(host)">🌐 {{ host }}</a></template>
|
||||||
<template v-else>🌐 Unbound host</template>
|
<template v-else>🌐 Unbound host</template>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="session-group-sessions">
|
<div class="session-list">
|
||||||
<div
|
<div
|
||||||
v-for="session in group.sessions"
|
v-for="session in group.sessions"
|
||||||
:key="session.id"
|
:key="session.id"
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
'is-linked-credential': hoveredCredentialUuid === session.credential_uuid
|
'is-linked-credential': hoveredCredentialUuid === session.credential_uuid
|
||||||
}]"
|
}]"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@mousedown.prevent
|
||||||
|
@click.capture="handleCardClick"
|
||||||
@focusin="handleSessionFocus(session)"
|
@focusin="handleSessionFocus(session)"
|
||||||
@focusout="handleSessionBlur($event)"
|
@focusout="handleSessionBlur($event)"
|
||||||
@keydown="handleDelete($event, session)"
|
@keydown="handleDelete($event, session)"
|
||||||
@@ -94,6 +96,13 @@ const handleSessionBlur = (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCardClick = (event) => {
|
||||||
|
if (!event.currentTarget.matches(':focus')) {
|
||||||
|
event.currentTarget.focus()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = (event, session) => {
|
const handleDelete = (event, session) => {
|
||||||
const apple = navigator.userAgent.includes('Mac OS')
|
const apple = navigator.userAgent.includes('Mac OS')
|
||||||
if (event.key === 'Delete' || apple && event.key === 'Backspace') {
|
if (event.key === 'Delete' || apple && event.key === 'Backspace') {
|
||||||
@@ -159,55 +168,18 @@ const groupedSessions = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.session-meta-info {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
[data-component="session-list-section"] .session-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5em;
|
|
||||||
}
|
|
||||||
.session-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
.session-group-host {
|
.session-group-host {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0.5rem 0 0.5rem -1.2rem;
|
||||||
}
|
|
||||||
.session-group-host a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.session-group-host a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
.session-group-host.is-current-site {
|
.session-group-host.is-current-site {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
.session-group-sessions {
|
.btn-card-delete {
|
||||||
display: grid;
|
display: none;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(var(--card-width), 1fr));
|
|
||||||
gap: 0.5em;
|
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
.session-group-sessions .session-item {
|
.session-item:focus .btn-card-delete {
|
||||||
width: auto;
|
display: block;
|
||||||
height: auto;
|
|
||||||
padding: 0.75rem;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.session-group-sessions .session-item .item-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.session-group-sessions .session-item .item-details {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
.session-group-sessions .session-item .session-dates {
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@
|
|||||||
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
|
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
|
||||||
<div class="role-line" v-if="roleName">{{ roleName }}</div>
|
<div class="role-line" v-if="roleName">{{ roleName }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="info-label"><strong>Visits:</strong></span>
|
<div class="user-details">
|
||||||
<span class="info-value">{{ visits || 0 }}</span>
|
<span class="date-label"><strong>Visits:</strong></span>
|
||||||
<span class="info-label"><strong>Registered:</strong></span>
|
<span class="date-value">{{ visits || 0 }}</span>
|
||||||
<span class="info-value">{{ formatDate(createdAt) }}</span>
|
<span class="date-label"><strong>Registered:</strong></span>
|
||||||
<span class="info-label"><strong>Last seen:</strong></span>
|
<span class="date-value">{{ formatDate(createdAt) }}</span>
|
||||||
<span class="info-value">{{ formatDate(lastSeen) }}</span>
|
<span class="date-label"><strong>Last seen:</strong></span>
|
||||||
|
<span class="date-value">{{ formatDate(lastSeen) }}</span>
|
||||||
|
</div>
|
||||||
<div v-if="$slots.default" class="user-info-extra">
|
<div v-if="$slots.default" class="user-info-extra">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +70,7 @@ const userLoaded = computed(() => !!props.name)
|
|||||||
"label3 value3";
|
"label3 value3";
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 720px) {
|
||||||
.user-info.has-extra {
|
.user-info.has-extra {
|
||||||
grid-template-columns: auto 1fr 2fr;
|
grid-template-columns: auto 1fr 2fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@@ -80,7 +82,7 @@ const userLoaded = computed(() => !!props.name)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name-heading { grid-area: heading; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin: 0 0 0.25rem 0; }
|
.user-name-heading { grid-area: heading; display: flex; align-items: center; flex-wrap: wrap; margin: 0 0 0.25rem 0; }
|
||||||
.org-role-sub { grid-area: org; display:flex; flex-direction:column; margin: -0.15rem 0 0.25rem; }
|
.org-role-sub { grid-area: org; display:flex; flex-direction:column; margin: -0.15rem 0 0.25rem; }
|
||||||
.org-line { font-size: .7rem; font-weight:600; line-height:1.1; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
.org-line { font-size: .7rem; font-weight:600; line-height:1.1; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
.role-line { font-size:.65rem; color: var(--color-text-muted); line-height:1.1; }
|
.role-line { font-size:.65rem; color: var(--color-text-muted); line-height:1.1; }
|
||||||
@@ -93,15 +95,14 @@ const userLoaded = computed(() => !!props.name)
|
|||||||
.user-info-extra { grid-area: extra; padding-left: 2rem; border-left: 1px solid var(--color-border); }
|
.user-info-extra { grid-area: extra; padding-left: 2rem; border-left: 1px solid var(--color-border); }
|
||||||
.user-name-row { display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; }
|
.user-name-row { display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; }
|
||||||
.user-name-row.editing { flex: 1 1 auto; }
|
.user-name-row.editing { flex: 1 1 auto; }
|
||||||
.icon { flex: 0 0 auto; }
|
.icon { flex: 0 0 auto; font-size: 1.5em; width: 40px; }
|
||||||
.display-name { font-weight: 600; font-size: 1.05em; line-height: 1.2; max-width: 14ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.display-name { font-weight: 600; font-size: 1.05em; line-height: 1.2; max-width: 14ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.name-input { width: auto; flex: 1 1 140px; min-width: 120px; padding: 6px 8px; font-size: 0.9em; border: 1px solid var(--color-border-strong); border-radius: 6px; background: var(--color-surface); color: var(--color-text); }
|
.name-input { width: auto; flex: 1 1 140px; min-width: 120px; padding: 6px 8px; font-size: 0.9em; border: 1px solid var(--color-border-strong); border-radius: 6px; background: var(--color-surface); color: var(--color-text); }
|
||||||
.user-name-heading .name-input { width: auto; }
|
.user-name-heading .name-input { width: auto; }
|
||||||
.name-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--focus-ring); }
|
.name-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--focus-ring); }
|
||||||
.mini-btn { width: auto; padding: 4px 6px; margin: 0; font-size: 0.75em; line-height: 1; background: var(--color-surface-muted); border: 1px solid var(--color-border-strong); border-radius: 6px; cursor: pointer; transition: background 0.2s, transform 0.15s, color 0.2s ease; color: var(--color-text); }
|
.mini-btn { width: auto; padding: 4px 6px; margin: 0; font-size: 0.75em; line-height: 1; cursor: pointer; }
|
||||||
.mini-btn:hover:not(:disabled) { background: var(--color-accent-soft); color: var(--color-accent); }
|
.mini-btn:hover:not(:disabled) { background: var(--color-accent-soft); color: var(--color-accent); }
|
||||||
.mini-btn:active:not(:disabled) { transform: translateY(1px); }
|
.mini-btn:active:not(:disabled) { transform: translateY(1px); }
|
||||||
.mini-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
.mini-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
@media (max-width: 768px) { .user-info-extra { padding-left: 0; padding-top: 1rem; border-left: none; border-top: 1px solid var(--color-border); } }
|
@media (max-width: 720px) { .user-info-extra { padding-left: 0; padding-top: 1rem; margin-top: 1rem; border-left: none; border-top: 1px solid var(--color-border); } }
|
||||||
@media (max-width: 480px) { .user-name-heading { flex-direction: column; align-items: flex-start; } .user-name-row.editing { width: 100%; } .display-name { max-width: 100%; } }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user