Add aria-labels to form controls (selects, checkboxes, switches), set html lang attribute and page title, fix color contrast for --candle-dim and --text-faint tokens, underline inline links, remove opacity hack. Harden dev login endpoints with atomic findOneAndUpdate and tokenVersion in JWT. Update Playwright timeouts and E2E test helpers.
854 lines
24 KiB
Vue
854 lines
24 KiB
Vue
<template>
|
|
<div class="profile-page">
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="loading-state">
|
|
<p style="color: var(--text-faint)">Loading your profile...</p>
|
|
</div>
|
|
|
|
<!-- Unauthenticated State -->
|
|
<div v-else-if="!memberData" class="loading-state">
|
|
<p style="color: var(--text-faint); margin-bottom: 12px;">Please sign in to access your profile settings.</p>
|
|
<button
|
|
class="btn btn-primary"
|
|
@click="openLoginModal({ title: 'Sign in to your profile', description: 'Enter your email to manage your profile settings' })"
|
|
>
|
|
Sign In
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="profile-authenticated">
|
|
<!-- PAGE HEADER -->
|
|
<PageHeader
|
|
title="Edit Profile"
|
|
subtitle="How you appear to other members"
|
|
/>
|
|
|
|
<!-- TWO-COLUMN FORM -->
|
|
<form class="page-content" @submit.prevent="handleSubmit">
|
|
<div class="profile-main">
|
|
<div class="profile-columns">
|
|
|
|
<!-- ======== LEFT COLUMN ======== -->
|
|
<div class="profile-col-left">
|
|
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">Basics</div>
|
|
|
|
<div class="field">
|
|
<label>Name</label>
|
|
<input v-model="formData.name" type="text" placeholder="Your name" required />
|
|
</div>
|
|
|
|
<div class="row-2">
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Pronouns</label>
|
|
<PrivacyToggle v-model="formData.pronounsPrivacy" />
|
|
</div>
|
|
<input v-model="formData.pronouns" type="text" placeholder="e.g., she/her, they/them" />
|
|
</div>
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Timezone</label>
|
|
<PrivacyToggle v-model="formData.timeZonePrivacy" />
|
|
</div>
|
|
<input v-model="formData.timeZone" type="text" placeholder="e.g., America/Toronto" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Avatar</label>
|
|
<PrivacyToggle v-model="formData.avatarPrivacy" />
|
|
</div>
|
|
<div class="avatar-row">
|
|
<button
|
|
v-for="ghost in availableGhosts"
|
|
:key="ghost.value"
|
|
type="button"
|
|
class="avatar-option"
|
|
:class="{ selected: formData.avatar === ghost.value }"
|
|
:title="ghost.label"
|
|
@click="formData.avatar = ghost.value"
|
|
>
|
|
<img :src="ghost.image" :alt="ghost.label" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- About You -->
|
|
<hr class="section-divider" />
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">About You</div>
|
|
|
|
<div class="row-2">
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Studio / Organization</label>
|
|
<PrivacyToggle v-model="formData.studioPrivacy" />
|
|
</div>
|
|
<input v-model="formData.studio" type="text" placeholder="Studio name" />
|
|
</div>
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Location</label>
|
|
<PrivacyToggle v-model="formData.locationPrivacy" />
|
|
</div>
|
|
<input v-model="formData.location" type="text" placeholder="Toronto, ON" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>Bio</label>
|
|
<PrivacyToggle v-model="formData.bioPrivacy" />
|
|
</div>
|
|
<textarea v-model="formData.bio" rows="2" placeholder="Share your background, interests, and experience..." maxlength="300"></textarea>
|
|
<div class="char-count">{{ formData.bio?.length || 0 }} / 300</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Skills Exchange -->
|
|
<hr class="section-divider" />
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">Skills Exchange</div>
|
|
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>What I Can Contribute</label>
|
|
<PrivacyToggle v-model="formData.offeringPrivacy" />
|
|
</div>
|
|
<TagInput v-model="formData.offering.tags" placeholder="add skill..." />
|
|
</div>
|
|
<div class="field">
|
|
<label>Details</label>
|
|
<textarea v-model="formData.offering.text" rows="2" placeholder="e.g., I have 10+ years in Unity and love helping new devs."></textarea>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<div class="label-row">
|
|
<label>What I'm Looking For</label>
|
|
<PrivacyToggle v-model="formData.lookingForPrivacy" />
|
|
</div>
|
|
<TagInput v-model="formData.lookingFor.tags" placeholder="add topic..." />
|
|
</div>
|
|
<div class="field">
|
|
<label>Details</label>
|
|
<textarea v-model="formData.lookingFor.text" rows="2" placeholder="e.g., Seeking a business-minded co-founder for a worker co-op studio."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Visibility -->
|
|
<hr class="section-divider" />
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">Visibility</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch v-model="formData.showInDirectory" aria-label="Show in Member Directory" />
|
|
<div class="toggle-label">
|
|
Show in Member Directory
|
|
<span class="toggle-sub">Your profile will appear in the public member listing</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ======== RIGHT COLUMN ======== -->
|
|
<div class="profile-col-right">
|
|
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">Peer Support</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch v-model="formData.peerSupportEnabled" aria-label="Offer Peer Support" />
|
|
<div class="toggle-label">
|
|
Offer Peer Support
|
|
<span class="toggle-sub">Let other members request 1:1 time with you</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="formData.peerSupportEnabled" class="peer-panel">
|
|
<div class="field">
|
|
<label>Skill-Based Topics</label>
|
|
<TagInput v-model="formData.peerSupportSkillTopics" placeholder="add topic..." />
|
|
<div v-if="suggestedSkillTopics.length" class="suggested">
|
|
Suggested from your offerings:
|
|
<a
|
|
v-for="tag in suggestedSkillTopics"
|
|
:key="tag"
|
|
@click="addSuggestedSkillTopic(tag)"
|
|
>{{ tag }}</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Conversational Topics</label>
|
|
<div class="checkbox-grid">
|
|
<label
|
|
v-for="topic in availableSupportTopics"
|
|
:key="topic"
|
|
class="checkbox-item"
|
|
:class="{ checked: formData.peerSupportSupportTopics.includes(topic) }"
|
|
@click.prevent="toggleSupportTopic(topic)"
|
|
>
|
|
<span class="cb">✓</span>
|
|
{{ topic }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Availability</label>
|
|
<textarea v-model="formData.peerSupportAvailability" rows="2" placeholder="e.g. Weekday afternoons ET"></textarea>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Slack Handle</label>
|
|
<input v-model="formData.peerSupportSlackUsername" type="text" placeholder="@yourslackname" />
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Personal Message</label>
|
|
<textarea v-model="formData.peerSupportMessage" rows="2" maxlength="200" placeholder="Brief note shown to people requesting time with you"></textarea>
|
|
<div class="char-count">{{ formData.peerSupportMessage?.length || 0 }} / 200</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notifications -->
|
|
<hr class="section-divider" />
|
|
<div class="profile-col-inset">
|
|
<div class="section-label">Notifications</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch v-model="formData.notifyEvents" aria-label="Event reminders" />
|
|
<div class="toggle-label">
|
|
Event reminders
|
|
<span class="toggle-sub">Get notified about upcoming events</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch v-model="formData.notifyUpdates" aria-label="Community updates" />
|
|
<div class="toggle-label">
|
|
Community updates
|
|
<span class="toggle-sub">New posts from members you follow</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toggle-field">
|
|
<USwitch v-model="formData.notifyPeerRequests" aria-label="Peer support requests" />
|
|
<div class="toggle-label">
|
|
Peer support requests
|
|
<span class="toggle-sub">When someone wants to connect</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ======== SAVE BAR ======== -->
|
|
<div class="save-bar">
|
|
<button type="submit" class="btn btn-primary" :disabled="saving || !hasChanges">
|
|
{{ saving ? 'Saving...' : 'Save Profile' }}
|
|
</button>
|
|
<button type="button" class="btn" @click="resetForm">Reset Changes</button>
|
|
<span v-if="saveSuccess" class="save-msg save-msg-ok">Profile updated.</span>
|
|
<span v-if="saveError" class="save-msg save-msg-err">{{ saveError }}</span>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { memberData, checkMemberStatus } = useAuth()
|
|
const { openLoginModal } = useLoginModal()
|
|
|
|
// Available ghost avatars
|
|
const availableGhosts = [
|
|
{ value: 'disbelieving', label: 'Disbelieving', image: '/ghosties/Ghost-Disbelieving.png' },
|
|
{ value: 'double-take', label: 'Double Take', image: '/ghosties/Ghost-Double-Take.png' },
|
|
{ value: 'exasperated', label: 'Exasperated', image: '/ghosties/Ghost-Exasperated.png' },
|
|
{ value: 'mild', label: 'Mild', image: '/ghosties/Ghost-Mild.png' },
|
|
{ value: 'sweet', label: 'Sweet', image: '/ghosties/Ghost-Sweet.png' },
|
|
{ value: 'wtf', label: 'WTF', image: '/ghosties/Ghost-WTF.png' },
|
|
]
|
|
|
|
// Form state
|
|
const formData = reactive({
|
|
name: '',
|
|
pronouns: '',
|
|
timeZone: '',
|
|
avatar: '',
|
|
studio: '',
|
|
bio: '',
|
|
location: '',
|
|
offering: { text: '', tags: [] },
|
|
lookingFor: { text: '', tags: [] },
|
|
showInDirectory: true,
|
|
// Peer support
|
|
peerSupportEnabled: false,
|
|
peerSupportSkillTopics: [],
|
|
peerSupportSupportTopics: [],
|
|
peerSupportAvailability: '',
|
|
peerSupportMessage: '',
|
|
peerSupportSlackUsername: '',
|
|
// Privacy
|
|
pronounsPrivacy: 'members',
|
|
timeZonePrivacy: 'members',
|
|
avatarPrivacy: 'members',
|
|
studioPrivacy: 'members',
|
|
bioPrivacy: 'members',
|
|
locationPrivacy: 'members',
|
|
offeringPrivacy: 'members',
|
|
lookingForPrivacy: 'members',
|
|
// Notifications
|
|
notifyEvents: true,
|
|
notifyUpdates: true,
|
|
notifyPeerRequests: true,
|
|
})
|
|
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const saveSuccess = ref(false)
|
|
const saveError = ref(null)
|
|
const initialData = ref(null)
|
|
|
|
// Available conversational support topics
|
|
const availableSupportTopics = [
|
|
'Co-founder relationships',
|
|
'Burnout prevention',
|
|
'Impostor syndrome',
|
|
'Work-life boundaries',
|
|
'Conflict resolution',
|
|
'General chat & support',
|
|
]
|
|
|
|
// Computed
|
|
const hasChanges = computed(() => {
|
|
return JSON.stringify(formData) !== JSON.stringify(initialData.value)
|
|
})
|
|
|
|
const suggestedSkillTopics = computed(() => {
|
|
if (!formData.offering.tags?.length) return []
|
|
return formData.offering.tags.filter(
|
|
(t) => !formData.peerSupportSkillTopics?.includes(t)
|
|
)
|
|
})
|
|
|
|
// Toggle a support topic in/out of the selection
|
|
const toggleSupportTopic = (topic) => {
|
|
const idx = formData.peerSupportSupportTopics.indexOf(topic)
|
|
if (idx >= 0) {
|
|
formData.peerSupportSupportTopics.splice(idx, 1)
|
|
} else {
|
|
formData.peerSupportSupportTopics.push(topic)
|
|
}
|
|
}
|
|
|
|
const addSuggestedSkillTopic = (tag) => {
|
|
if (!Array.isArray(formData.peerSupportSkillTopics)) {
|
|
formData.peerSupportSkillTopics = []
|
|
}
|
|
if (!formData.peerSupportSkillTopics.includes(tag)) {
|
|
formData.peerSupportSkillTopics.push(tag)
|
|
}
|
|
}
|
|
|
|
// Load member data into form
|
|
const loadProfile = () => {
|
|
if (memberData.value) {
|
|
formData.name = memberData.value.name || ''
|
|
formData.pronouns = memberData.value.pronouns || ''
|
|
formData.timeZone = memberData.value.timeZone || ''
|
|
formData.avatar = memberData.value.avatar || ''
|
|
formData.studio = memberData.value.studio || ''
|
|
formData.bio = memberData.value.bio || ''
|
|
formData.location = memberData.value.location || ''
|
|
|
|
// Load offering (handle both old string and new object format)
|
|
if (typeof memberData.value.offering === 'string') {
|
|
formData.offering.text = memberData.value.offering
|
|
formData.offering.tags = []
|
|
} else if (memberData.value.offering) {
|
|
formData.offering.text = memberData.value.offering?.text || ''
|
|
formData.offering.tags = Array.isArray(memberData.value.offering?.tags)
|
|
? [...memberData.value.offering.tags]
|
|
: []
|
|
} else {
|
|
formData.offering.text = ''
|
|
formData.offering.tags = []
|
|
}
|
|
|
|
// Load lookingFor (handle both old string and new object format)
|
|
if (typeof memberData.value.lookingFor === 'string') {
|
|
formData.lookingFor.text = memberData.value.lookingFor
|
|
formData.lookingFor.tags = []
|
|
} else if (memberData.value.lookingFor) {
|
|
formData.lookingFor.text = memberData.value.lookingFor?.text || ''
|
|
formData.lookingFor.tags = Array.isArray(memberData.value.lookingFor?.tags)
|
|
? [...memberData.value.lookingFor.tags]
|
|
: []
|
|
} else {
|
|
formData.lookingFor.text = ''
|
|
formData.lookingFor.tags = []
|
|
}
|
|
|
|
formData.showInDirectory = memberData.value.showInDirectory ?? true
|
|
|
|
// Load peer support data
|
|
if (memberData.value.peerSupport) {
|
|
formData.peerSupportEnabled = memberData.value.peerSupport.enabled || false
|
|
formData.peerSupportSkillTopics = Array.isArray(memberData.value.peerSupport.skillTopics)
|
|
? [...memberData.value.peerSupport.skillTopics]
|
|
: []
|
|
formData.peerSupportSupportTopics = Array.isArray(memberData.value.peerSupport.supportTopics)
|
|
? [...memberData.value.peerSupport.supportTopics]
|
|
: []
|
|
formData.peerSupportAvailability = memberData.value.peerSupport.availability || ''
|
|
formData.peerSupportMessage = memberData.value.peerSupport.personalMessage || ''
|
|
formData.peerSupportSlackUsername = memberData.value.peerSupport.slackUsername || ''
|
|
} else {
|
|
formData.peerSupportEnabled = false
|
|
formData.peerSupportSkillTopics = []
|
|
formData.peerSupportSupportTopics = []
|
|
formData.peerSupportAvailability = ''
|
|
formData.peerSupportMessage = ''
|
|
formData.peerSupportSlackUsername = ''
|
|
}
|
|
|
|
// Load privacy settings (with defaults)
|
|
const privacy = memberData.value.privacy || {}
|
|
formData.pronounsPrivacy = privacy.pronouns || 'members'
|
|
formData.timeZonePrivacy = privacy.timeZone || 'members'
|
|
formData.avatarPrivacy = privacy.avatar || 'members'
|
|
formData.studioPrivacy = privacy.studio || 'members'
|
|
formData.bioPrivacy = privacy.bio || 'members'
|
|
formData.locationPrivacy = privacy.location || 'members'
|
|
formData.offeringPrivacy = privacy.offering || 'members'
|
|
formData.lookingForPrivacy = privacy.lookingFor || 'members'
|
|
|
|
// Load notification prefs
|
|
const notifs = memberData.value.notifications || {}
|
|
formData.notifyEvents = notifs.events ?? true
|
|
formData.notifyUpdates = notifs.updates ?? true
|
|
formData.notifyPeerRequests = notifs.peerRequests ?? true
|
|
|
|
// Store initial state for change detection
|
|
initialData.value = JSON.parse(JSON.stringify(formData))
|
|
}
|
|
}
|
|
|
|
// Handle form submission
|
|
const handleSubmit = async () => {
|
|
saving.value = true
|
|
saveSuccess.value = false
|
|
saveError.value = null
|
|
|
|
try {
|
|
// Save profile data
|
|
await $fetch('/api/members/profile', {
|
|
method: 'PATCH',
|
|
body: {
|
|
...formData,
|
|
notifications: {
|
|
events: formData.notifyEvents,
|
|
updates: formData.notifyUpdates,
|
|
peerRequests: formData.notifyPeerRequests,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Save peer support data separately
|
|
await $fetch('/api/members/me/peer-support', {
|
|
method: 'PATCH',
|
|
body: {
|
|
enabled: formData.peerSupportEnabled,
|
|
skillTopics: formData.peerSupportSkillTopics,
|
|
supportTopics: formData.peerSupportSupportTopics,
|
|
availability: formData.peerSupportAvailability,
|
|
personalMessage: formData.peerSupportMessage,
|
|
slackUsername: formData.peerSupportSlackUsername,
|
|
},
|
|
})
|
|
|
|
saveSuccess.value = true
|
|
|
|
// Refresh member data
|
|
await checkMemberStatus()
|
|
loadProfile()
|
|
|
|
setTimeout(() => { saveSuccess.value = false }, 3000)
|
|
} catch (error) {
|
|
console.error('Profile save error:', error)
|
|
saveError.value = error.data?.message || 'Failed to save profile. Please try again.'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
// Reset form to initial state
|
|
const resetForm = () => {
|
|
loadProfile()
|
|
saveSuccess.value = false
|
|
saveError.value = null
|
|
}
|
|
|
|
// Initialize on mount
|
|
onMounted(async () => {
|
|
if (!memberData.value) {
|
|
loading.value = true
|
|
const isAuthenticated = await checkMemberStatus()
|
|
loading.value = false
|
|
|
|
if (!isAuthenticated) {
|
|
openLoginModal({
|
|
title: 'Sign in to your profile',
|
|
description: 'Enter your email to manage your profile settings',
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
loadProfile()
|
|
})
|
|
|
|
useHead({
|
|
title: 'Edit Profile - Ghost Guild',
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.profile-page {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.profile-authenticated {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* ---- LOADING / EMPTY STATE ---- */
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.profile-page > .loading-state {
|
|
flex: 1;
|
|
}
|
|
|
|
/* ---- CONTENT AREA ---- */
|
|
.page-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Grid + save bar: one flex child so the center rule can span both */
|
|
.profile-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
position: relative;
|
|
}
|
|
|
|
/* Full-height vertical rule between columns (through save bar); 1fr | 1fr grid */
|
|
.profile-main::before {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 1025px) {
|
|
.profile-main::before {
|
|
display: block;
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 50%;
|
|
width: 0;
|
|
border-left: 1px dashed var(--border);
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
/* ---- TWO-COLUMN LAYOUT ---- */
|
|
.profile-columns {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: 1fr;
|
|
gap: 0;
|
|
align-items: stretch;
|
|
min-height: 0;
|
|
}
|
|
|
|
.profile-col-left,
|
|
.profile-col-right {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
align-self: stretch;
|
|
}
|
|
|
|
.profile-col-left {
|
|
border-right: none;
|
|
}
|
|
|
|
.profile-col-left > .profile-col-inset:first-of-type,
|
|
.profile-col-right > .profile-col-inset:first-of-type {
|
|
padding-top: 14px;
|
|
}
|
|
|
|
.profile-col-left .profile-col-inset {
|
|
padding-left: 28px;
|
|
padding-right: 24px;
|
|
}
|
|
|
|
.profile-col-right .profile-col-inset {
|
|
padding-left: 24px;
|
|
padding-right: 28px;
|
|
}
|
|
|
|
/* ---- MULTI-COLUMN ROWS ---- */
|
|
.row-2 {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 14px;
|
|
}
|
|
|
|
/* ---- LABEL WITH PRIVACY ---- */
|
|
.label-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.label-row label {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* ---- AVATAR PICKER ---- */
|
|
.avatar-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
|
|
.avatar-option {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px dashed var(--border);
|
|
background: var(--bg);
|
|
cursor: pointer;
|
|
padding: 3px;
|
|
transition: all 0.12s;
|
|
}
|
|
|
|
.avatar-option img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.avatar-option:hover {
|
|
border-color: var(--candle-dim);
|
|
background: var(--surface-hover);
|
|
}
|
|
|
|
.avatar-option.selected {
|
|
border: 2px solid var(--candle);
|
|
background: var(--surface);
|
|
}
|
|
|
|
/* ---- TEXTAREA RESIZE ---- */
|
|
.field textarea {
|
|
resize: vertical;
|
|
}
|
|
|
|
/* ---- CHAR COUNT ---- */
|
|
.char-count {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
text-align: right;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* ---- TOGGLE FIELD ---- */
|
|
.toggle-field {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.toggle-label {
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.toggle-sub {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
/* ---- CHECKBOX GRID ---- */
|
|
.checkbox-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 3px 12px;
|
|
}
|
|
|
|
.checkbox-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
padding: 2px 0;
|
|
user-select: none;
|
|
}
|
|
|
|
.checkbox-item .cb {
|
|
width: 13px;
|
|
height: 13px;
|
|
border: 1px dashed var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 9px;
|
|
color: transparent;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.checkbox-item.checked .cb {
|
|
border-color: var(--candle);
|
|
border-style: solid;
|
|
color: var(--candle);
|
|
}
|
|
|
|
.checkbox-item:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
/* ---- PEER SUPPORT PANEL ---- */
|
|
.peer-panel {
|
|
border: 1px dashed var(--border);
|
|
padding: 12px 14px;
|
|
margin-top: 4px;
|
|
margin-bottom: 12px;
|
|
background: var(--surface);
|
|
}
|
|
|
|
.peer-panel .suggested {
|
|
font-size: 10px;
|
|
color: var(--text-faint);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.peer-panel .suggested a {
|
|
color: var(--candle);
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
/* ---- DISABLED BUTTON ---- */
|
|
.btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ---- SAVE BAR ---- */
|
|
.save-bar {
|
|
flex-shrink: 0;
|
|
padding: 24px 28px 24px;
|
|
margin-top: 0;
|
|
border-top: 1px dashed var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.save-msg {
|
|
font-size: 11px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.save-msg-ok {
|
|
color: var(--green, var(--candle));
|
|
}
|
|
|
|
.save-msg-err {
|
|
color: var(--ember);
|
|
}
|
|
|
|
/* ---- RESPONSIVE ---- */
|
|
@media (max-width: 1024px) {
|
|
.profile-columns {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto;
|
|
}
|
|
|
|
.profile-col-left {
|
|
border-right: none;
|
|
border-bottom: 1px dashed var(--border);
|
|
padding-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.profile-col-left .profile-col-inset,
|
|
.profile-col-right .profile-col-inset {
|
|
padding-left: 28px;
|
|
padding-right: 28px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.row-2 {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.checkbox-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.profile-col-left .profile-col-inset,
|
|
.profile-col-right .profile-col-inset {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
}
|
|
|
|
.save-bar {
|
|
padding-left: 16px;
|
|
padding-right: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
}
|
|
</style>
|