New user basic info card and improved layout, telephone number and other contact details shown.
This commit is contained in:
@@ -184,12 +184,14 @@ defineExpose({ focusFirstElement })
|
||||
:visits="userDetail.visits"
|
||||
:created-at="userDetail.created_at"
|
||||
:last-seen="userDetail.last_seen"
|
||||
:email="userDetail.email"
|
||||
:telephone="userDetail.telephone"
|
||||
:loading="loading"
|
||||
:org-display-name="userDetail.org.display_name"
|
||||
:role-name="userDetail.role"
|
||||
:update-endpoint="`/auth/api/admin/users/${selectedUser.uuid}/display-name`"
|
||||
@saved="$emit('onUserNameSaved')"
|
||||
@edit-name="handleEditName"
|
||||
@edit="handleEditName"
|
||||
>
|
||||
<div class="admin-actions">
|
||||
<button
|
||||
|
||||
@@ -325,6 +325,7 @@ input:not([type]),
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="email"],
|
||||
input[type="tel"],
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
@@ -341,6 +342,7 @@ input:not([type]):focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
:visits="authStore.userInfo?.visits || 0"
|
||||
:created-at="authStore.userInfo?.created_at"
|
||||
:last-seen="authStore.userInfo?.last_seen"
|
||||
:email="ctx.user.email"
|
||||
:telephone="ctx.user.telephone"
|
||||
:org-display-name="orgDisplayName"
|
||||
:role-name="roleDisplayName"
|
||||
:can-edit="false"
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
:name="authStore.userInfo.ctx.user.display_name"
|
||||
:email="authStore.userInfo.ctx.user.email"
|
||||
:preferred_username="authStore.userInfo.ctx.user.preferred_username"
|
||||
:telephone="authStore.userInfo.ctx.user.telephone"
|
||||
:visits="authStore.userInfo.visits"
|
||||
:created-at="authStore.userInfo.created_at"
|
||||
:last-seen="authStore.userInfo.last_seen"
|
||||
:loading="authStore.isLoading"
|
||||
:org-display-name="authStore.userInfo.ctx.org.display_name"
|
||||
:role-name="authStore.userInfo.ctx.role.display_name"
|
||||
update-endpoint="/auth/api/user/display-name"
|
||||
@saved="authStore.loadUserInfo()"
|
||||
@edit-name="openNameDialog"
|
||||
@edit-email="openEmailDialog"
|
||||
@edit-preferred-username="openPreferredUsernameDialog"
|
||||
@edit="openEditDialog"
|
||||
@keydown="handleUserInfoKeydown"
|
||||
>
|
||||
<div class="remote-auth-inline">
|
||||
@@ -40,7 +41,7 @@
|
||||
@device-info-visible="showDeviceInfo = $event"
|
||||
/>
|
||||
</div>
|
||||
<p class="remote-auth-description">Provided by another device requesting remote auth.</p>
|
||||
<p class="remote-auth-description">Login from another device</p>
|
||||
</UserBasicInfo>
|
||||
</section>
|
||||
|
||||
@@ -83,44 +84,28 @@
|
||||
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">
|
||||
<h3>Edit Display Name</h3>
|
||||
<form @submit.prevent="saveName" class="modal-form">
|
||||
<NameEditForm
|
||||
label="Display Name"
|
||||
v-model="newName"
|
||||
:busy="saving"
|
||||
@cancel="showNameDialog = false"
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-if="showEmailDialog" @close="showEmailDialog = false">
|
||||
<h3>Edit Email</h3>
|
||||
<form @submit.prevent="saveEmail" class="modal-form">
|
||||
<NameEditForm
|
||||
label="Email"
|
||||
placeholder="user@example.com"
|
||||
input-type="email"
|
||||
v-model="newEmail"
|
||||
:busy="saving"
|
||||
:error="emailError"
|
||||
@cancel="showEmailDialog = false"
|
||||
/>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-if="showPreferredUsernameDialog" @close="showPreferredUsernameDialog = false">
|
||||
<h3>Edit Preferred Username</h3>
|
||||
<form @submit.prevent="savePreferredUsername" class="modal-form">
|
||||
<NameEditForm
|
||||
label="Preferred Username"
|
||||
placeholder="username"
|
||||
v-model="newPreferredUsername"
|
||||
:busy="saving"
|
||||
:error="preferredUsernameError"
|
||||
@cancel="showPreferredUsernameDialog = false"
|
||||
/>
|
||||
<Modal v-if="showEditDialog" @close="showEditDialog = false">
|
||||
<h3>Edit Profile</h3>
|
||||
<form @submit.prevent="saveProfile" class="modal-form">
|
||||
<div class="profile-edit-form">
|
||||
<label for="edit-display-name">Display Name
|
||||
<input id="edit-display-name" type="text" v-model="editName" :disabled="saving" required />
|
||||
</label>
|
||||
<label for="edit-email">Email
|
||||
<input id="edit-email" type="email" v-model="editEmail" :disabled="saving" placeholder="user@example.com" />
|
||||
</label>
|
||||
<label for="edit-username">Preferred Username
|
||||
<input id="edit-username" type="text" v-model="editUsername" :disabled="saving" placeholder="username" />
|
||||
</label>
|
||||
<label for="edit-telephone">Telephone
|
||||
<input id="edit-telephone" type="tel" v-model="editTelephone" :disabled="saving" placeholder="+1-555-123-4567" />
|
||||
</label>
|
||||
<div v-if="editError" class="error small">{{ editError }}</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" @click="showEditDialog = false" :disabled="saving">Cancel</button>
|
||||
<button type="submit" class="btn-primary" :disabled="saving" data-nav-primary>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -162,7 +147,6 @@ import CredentialList from '@/components/CredentialList.vue'
|
||||
import ThemeSelector from '@/components/ThemeSelector.vue'
|
||||
import UserBasicInfo from '@/components/UserBasicInfo.vue'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import NameEditForm from '@/components/NameEditForm.vue'
|
||||
import SessionList from '@/components/SessionList.vue'
|
||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||
import RemoteAuthPermit from '@/components/RemoteAuthPermit.vue'
|
||||
@@ -175,16 +159,14 @@ import { navigateButtonRow, focusPreferred, focusAtIndex, getDirection } from '@
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const updateInterval = ref(null)
|
||||
const showNameDialog = ref(false)
|
||||
const showEmailDialog = ref(false)
|
||||
const showPreferredUsernameDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const showRegLink = ref(false)
|
||||
const newName = ref('')
|
||||
const newEmail = ref('')
|
||||
const newPreferredUsername = ref('')
|
||||
const editName = ref('')
|
||||
const editEmail = ref('')
|
||||
const editUsername = ref('')
|
||||
const editTelephone = ref('')
|
||||
const saving = ref(false)
|
||||
const emailError = ref('')
|
||||
const preferredUsernameError = ref('')
|
||||
const editError = ref('')
|
||||
const hoveredCredentialUuid = ref(null)
|
||||
const hoveredSession = ref(null)
|
||||
const showDeviceInfo = ref(false)
|
||||
@@ -198,20 +180,16 @@ const userBasicInfo = ref(null)
|
||||
const userInfoSection = ref(null)
|
||||
|
||||
// Check if any modal/dialog is open (blocks arrow key navigation)
|
||||
const hasActiveModal = computed(() => showNameDialog.value || showEmailDialog.value || showPreferredUsernameDialog.value || showRegLink.value)
|
||||
const hasActiveModal = computed(() => showEditDialog.value || showRegLink.value)
|
||||
|
||||
watch(showNameDialog, (newVal) => { if (newVal) newName.value = authStore.userInfo?.ctx.user.display_name ?? '' })
|
||||
watch(showEmailDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
newEmail.value = authStore.userInfo?.ctx.user.email ?? ''
|
||||
emailError.value = ''
|
||||
}
|
||||
})
|
||||
watch(showPreferredUsernameDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
newPreferredUsername.value = authStore.userInfo?.ctx.user.preferred_username ?? ''
|
||||
preferredUsernameError.value = ''
|
||||
}
|
||||
watch(showEditDialog, (open) => {
|
||||
if (!open) return
|
||||
const user = authStore.userInfo?.ctx?.user
|
||||
editName.value = user?.display_name ?? ''
|
||||
editEmail.value = user?.email ?? ''
|
||||
editUsername.value = user?.preferred_username ?? ''
|
||||
editTelephone.value = user?.telephone ?? ''
|
||||
editError.value = ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -385,7 +363,7 @@ const terminateSession = async (session) => {
|
||||
|
||||
const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
|
||||
const logout = async () => { await authStore.logout() }
|
||||
const openNameDialog = () => { newName.value = authStore.userInfo?.ctx.user.display_name ?? ''; showNameDialog.value = true }
|
||||
const openEditDialog = () => { showEditDialog.value = true }
|
||||
const isAdmin = computed(() => {
|
||||
const perms = authStore.userInfo?.ctx.permissions
|
||||
return perms.includes('auth:admin') || perms.includes('auth:org:admin')
|
||||
@@ -409,57 +387,34 @@ const useWideLayout = computed(() => {
|
||||
})
|
||||
const breadcrumbEntries = computed(() => { const entries = [{ label: 'Auth', href: makeUiHref() }]; if (isAdmin.value) entries.push({ label: 'Admin', href: adminUiPath() }); return entries })
|
||||
|
||||
const saveName = async () => {
|
||||
const name = newName.value.trim()
|
||||
if (!name) { authStore.showMessage('Name cannot be empty', 'error'); return }
|
||||
const saveProfile = async () => {
|
||||
const name = editName.value.trim()
|
||||
if (!name) { editError.value = 'Name cannot be empty'; return }
|
||||
const user = authStore.userInfo.ctx.user
|
||||
const emailVal = editEmail.value.trim() || null
|
||||
const usernameVal = editUsername.value.trim() || null
|
||||
const telephoneVal = editTelephone.value.trim() || null
|
||||
try {
|
||||
editError.value = ''
|
||||
saving.value = true
|
||||
await apiJson('/auth/api/user/display-name', { method: 'PATCH', body: { display_name: name } })
|
||||
showNameDialog.value = false
|
||||
await authStore.loadUserInfo()
|
||||
authStore.showMessage('Name updated successfully!', 'success', 3000)
|
||||
} catch (e) { authStore.showMessage(e.message || 'Failed to update name', 'error') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
const openEmailDialog = () => {
|
||||
showEmailDialog.value = true
|
||||
}
|
||||
|
||||
const saveEmail = async () => {
|
||||
try {
|
||||
emailError.value = ''
|
||||
saving.value = true
|
||||
const email = newEmail.value.trim() || null
|
||||
await apiJson('/auth/api/user/email', { method: 'PATCH', body: { email } })
|
||||
showEmailDialog.value = false
|
||||
await authStore.loadUserInfo()
|
||||
authStore.showMessage('Email updated successfully!', 'success', 3000)
|
||||
const tasks = []
|
||||
if (name !== user.display_name)
|
||||
tasks.push(apiJson('/auth/api/user/display-name', { method: 'PATCH', body: { display_name: name } }))
|
||||
if (emailVal !== (user.email || null))
|
||||
tasks.push(apiJson('/auth/api/user/email', { method: 'PATCH', body: { email: emailVal } }))
|
||||
if (usernameVal !== (user.preferred_username || null))
|
||||
tasks.push(apiJson('/auth/api/user/preferred-username', { method: 'PATCH', body: { preferred_username: usernameVal } }))
|
||||
if (telephoneVal !== (user.telephone || null))
|
||||
tasks.push(apiJson('/auth/api/user/telephone', { method: 'PATCH', body: { telephone: telephoneVal } }))
|
||||
if (tasks.length) {
|
||||
await Promise.all(tasks)
|
||||
await authStore.loadUserInfo()
|
||||
authStore.showMessage('Profile updated!', 'success', 3000)
|
||||
}
|
||||
showEditDialog.value = false
|
||||
} catch (e) {
|
||||
emailError.value = e.message || 'Failed to update email'
|
||||
authStore.showMessage(emailError.value, 'error')
|
||||
}
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
const openPreferredUsernameDialog = () => {
|
||||
showPreferredUsernameDialog.value = true
|
||||
}
|
||||
|
||||
const savePreferredUsername = async () => {
|
||||
try {
|
||||
preferredUsernameError.value = ''
|
||||
saving.value = true
|
||||
const preferred_username = newPreferredUsername.value.trim() || null
|
||||
await apiJson('/auth/api/user/preferred-username', { method: 'PATCH', body: { preferred_username } })
|
||||
showPreferredUsernameDialog.value = false
|
||||
await authStore.loadUserInfo()
|
||||
authStore.showMessage('Preferred username updated successfully!', 'success', 3000)
|
||||
} catch (e) {
|
||||
preferredUsernameError.value = e.message || 'Failed to update preferred username'
|
||||
authStore.showMessage(preferredUsernameError.value, 'error')
|
||||
}
|
||||
finally { saving.value = false }
|
||||
editError.value = e.message || 'Failed to update profile'
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -474,4 +429,5 @@ const savePreferredUsername = async () => {
|
||||
.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); }
|
||||
.theme-toggle { position: absolute; top: var(--layout-padding); right: var(--layout-padding); }
|
||||
.profile-edit-form { display: flex; flex-direction: column; gap: var(--space-md); }
|
||||
</style>
|
||||
|
||||
@@ -771,7 +771,6 @@ defineExpose({ reset, deny, code, handleInput, loading, error })
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
<template>
|
||||
<div v-if="userLoaded" class="user-info" :class="{ 'has-extra': $slots.default }">
|
||||
<h3 class="user-name-heading">
|
||||
<span class="icon">👤</span>
|
||||
<span class="user-name-row">
|
||||
<span class="display-name" :title="name">{{ name }}</span>
|
||||
<button v-if="canEdit && updateEndpoint" class="mini-btn" @click="emit('editName')" title="Edit name">✏️</button>
|
||||
</span>
|
||||
</h3>
|
||||
<div v-if="orgDisplayName || roleName" class="org-role-sub">
|
||||
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
|
||||
<div class="role-line" v-if="roleName">{{ roleName }}</div>
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="date-label"><strong>Visits:</strong></span>
|
||||
<span class="date-value">{{ visits || 0 }}</span>
|
||||
<span class="date-label"><strong>Registered:</strong></span>
|
||||
<span class="date-value">{{ formatDate(createdAt) }}</span>
|
||||
<span class="date-label"><strong>Last seen:</strong></span>
|
||||
<span class="date-value">{{ formatDate(lastSeen) }}</span>
|
||||
</div>
|
||||
<div class="oidc-info">
|
||||
<div class="oidc-field">
|
||||
<span class="field-label"><strong>Email:</strong></span>
|
||||
<span class="field-value">{{ email || '—' }}</span>
|
||||
<button v-if="canEdit" class="mini-btn" @click="emit('editEmail')" title="Edit email">✏️</button>
|
||||
<div class="user-info-content">
|
||||
<div class="user-picture">
|
||||
<span>👤</span>
|
||||
</div>
|
||||
<div class="oidc-field">
|
||||
<span class="field-label"><strong>Preferred username:</strong></span>
|
||||
<span class="field-value">{{ preferred_username || '—' }}</span>
|
||||
<button v-if="canEdit" class="mini-btn" @click="emit('editPreferredUsername')" title="Edit preferred username">✏️</button>
|
||||
<h3 class="user-name-heading">
|
||||
<span class="user-name-row">
|
||||
<span class="display-name" :title="name">{{ name }}</span>
|
||||
<button v-if="canEdit && updateEndpoint" class="mini-btn" @click="emit('edit')" title="Edit profile">✏️</button>
|
||||
</span>
|
||||
</h3>
|
||||
<div v-if="orgDisplayName || roleName" class="org-role-sub">
|
||||
<div class="org-line" v-if="orgDisplayName">{{ orgDisplayName }}</div>
|
||||
<div class="role-line" v-if="roleName">{{ roleName }}</div>
|
||||
</div>
|
||||
<div class="info-fields-block">
|
||||
<div v-if="preferred_username" class="contact-item">🆔 {{ preferred_username }}</div>
|
||||
<a v-if="email" :href="`mailto:${email}`" class="contact-link">✉️ {{ email }}</a>
|
||||
<a v-if="telephone" :href="`tel:${telephone}`" class="contact-link">📞 {{ telephone }}</a>
|
||||
</div>
|
||||
<div class="info-line">
|
||||
<span v-if="visits">
|
||||
<span class="info-date">{{ formatDate(createdAt) }}</span>
|
||||
<span class="info-punct"> – </span>
|
||||
<span class="info-date">{{ formatDate(lastSeen) }}</span>
|
||||
<span class="info-punct"> ×</span>
|
||||
<span class="info-count">{{ visits }}</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="info-label">Created </span>
|
||||
<span class="info-date">{{ formatDate(createdAt) }}</span>
|
||||
<span class="info-punct"> — Never signed in</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.default" class="user-info-extra">
|
||||
@@ -38,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatDate } from '@/utils/helpers'
|
||||
|
||||
@@ -46,6 +49,7 @@ const props = defineProps({
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, default: null },
|
||||
preferred_username: { type: String, default: null },
|
||||
telephone: { type: String, default: null },
|
||||
visits: { type: [Number, String], default: 0 },
|
||||
createdAt: { type: [String, Number, Date], default: null },
|
||||
lastSeen: { type: [String, Number, Date], default: null },
|
||||
@@ -56,7 +60,7 @@ const props = defineProps({
|
||||
roleName: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['saved', 'editName', 'editEmail', 'editPreferredUsername'])
|
||||
const emit = defineEmits(['saved', 'edit'])
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const userLoaded = computed(() => !!props.name)
|
||||
@@ -64,62 +68,56 @@ const userLoaded = computed(() => !!props.name)
|
||||
|
||||
<style scoped>
|
||||
.user-info.has-extra {
|
||||
grid-template-columns: auto 1fr 2fr;
|
||||
grid-template-columns: minmax(0, 1fr) 14rem;
|
||||
grid-template-areas:
|
||||
"heading heading extra"
|
||||
"org org extra"
|
||||
"label1 value1 extra"
|
||||
"label2 value2 extra"
|
||||
"label3 value3 extra"
|
||||
"oidc oidc extra";
|
||||
"content extra";
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.user-info:not(.has-extra) {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"heading heading"
|
||||
"org org"
|
||||
"label1 value1"
|
||||
"label2 value2"
|
||||
"label3 value3"
|
||||
"oidc oidc";
|
||||
"content";
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.user-info.has-extra {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"heading heading"
|
||||
"org org"
|
||||
"label1 value1"
|
||||
"label2 value2"
|
||||
"label3 value3"
|
||||
"extra extra";
|
||||
"content"
|
||||
"extra";
|
||||
}
|
||||
}
|
||||
|
||||
.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-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; }
|
||||
.info-label:nth-of-type(1) { grid-area: label1; }
|
||||
.info-value:nth-of-type(2) { grid-area: value1; }
|
||||
.info-label:nth-of-type(3) { grid-area: label2; }
|
||||
.info-value:nth-of-type(4) { grid-area: value2; }
|
||||
.info-label:nth-of-type(5) { grid-area: label3; }
|
||||
.info-value:nth-of-type(6) { grid-area: value3; }
|
||||
.oidc-info { grid-area: oidc; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border); display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.oidc-field { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9em; }
|
||||
.oidc-field .field-label { flex-shrink: 0; }
|
||||
.oidc-field .field-value { flex: 1; word-break: break-all; color: var(--color-text-muted); }
|
||||
.oidc-field .mini-btn { flex-shrink: 0; }
|
||||
.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.editing { flex: 1 1 auto; }
|
||||
.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); }
|
||||
.user-name-heading .name-input { width: auto; }
|
||||
.name-input:focus { outline: none; border-color: var(--color-accent); box-shadow: var(--focus-ring); }
|
||||
.user-info-content {
|
||||
grid-area: content;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"picture heading fields"
|
||||
"picture org fields"
|
||||
". info info";
|
||||
gap: 0 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-picture { grid-area: picture; display: flex; align-items: flex-start; font-size: 2em; line-height: 1; }
|
||||
.user-name-heading { grid-area: heading; display: flex; align-items: center; flex-wrap: wrap; margin: 0 0 0.25rem 0; min-width: 0; }
|
||||
.org-role-sub { grid-area: org; display: flex; flex-direction: column; min-width: 0; }
|
||||
.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; }
|
||||
.info-fields-block { grid-area: fields; display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
|
||||
.contact-item { display: block; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.contact-link { color: var(--color-text); text-decoration: none; display: block; transition: transform 0.1s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.contact-link:hover { transform: scale(1.05); }
|
||||
.info-line { grid-area: info; line-height: 1.4; font-size: 0.9em; }
|
||||
.info-date { color: var(--color-text) !important; }
|
||||
.info-label { color: var(--color-text) !important; }
|
||||
.info-punct { color: var(--color-text-muted) !important; }
|
||||
.info-count { color: var(--color-text-muted) !important; }
|
||||
.user-info-extra { grid-area: extra; padding-left: 1rem; border-left: 1px solid var(--color-border); flex-shrink: 0; }
|
||||
.user-name-row { display: inline-flex; align-items: center; gap: 0.35rem; max-width: 100%; min-width: 0; }
|
||||
.display-name { font-weight: 600; font-size: 1.05em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.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:active:not(:disabled) { transform: translateY(1px); }
|
||||
|
||||
Reference in New Issue
Block a user