wip bsky client for the web & android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(pluralhost): add member info dialog

vi 3562cc62 c3a91649

+325 -85
+20
src/assets/main.css
··· 94 94 --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.1); 95 95 --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); 96 96 --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); 97 + 98 + --border-colour: hsl(var(--surface0) / 0.5); 99 + 100 + --accented-border-colour: color-mix( 101 + in oklab, 102 + hsl(var(--accent) / 0.1) 20%, 103 + hsl(var(--surface0) / 0.1) 80% 104 + ); 105 + 106 + --accent-surface: color-mix(in oklab, hsl(var(--accent) / 1) 5%, hsl(var(--mantle) / 1) 95%); 107 + --accent-surface-hover: color-mix( 108 + in oklab, 109 + hsl(var(--accent) / 1) 10%, 110 + hsl(var(--mantle) / 1) 90% 111 + ); 112 + --accent-surface-pressed: color-mix( 113 + in oklab, 114 + hsl(var(--accent) / 1) 2%, 115 + hsl(var(--mantle) / 1) 98% 116 + ); 97 117 } 98 118 99 119 body {
-73
src/components/Modals/Plurality/PluralHelp.vue
··· 120 120 } 121 121 } 122 122 } 123 - 124 - /* .info-section { 125 - background: hsla(var(--surface1) / 0.1); 126 - border-radius: 1rem; 127 - padding: 1rem; 128 - text-align: left; 129 - 130 - .info-item { 131 - display: flex; 132 - gap: 1rem; 133 - align-items: flex-start; 134 - 135 - .info-icon { 136 - font-size: 1.5rem; 137 - color: hsl(var(--accent)); 138 - flex-shrink: 0; 139 - } 140 - 141 - .info-text { 142 - strong { 143 - display: block; 144 - margin-bottom: 0.25rem; 145 - font-size: 0.9rem; 146 - } 147 - p { 148 - font-size: 0.85rem; 149 - color: hsl(var(--subtext0)); 150 - line-height: 1.4; 151 - } 152 - } 153 - } 154 - } 155 - 156 - .modal-actions { 157 - display: flex; 158 - justify-content: center; 159 - gap: 0.75rem; 160 - 161 - .action-card { 162 - display: flex; 163 - flex-direction: column; 164 - align-items: center; 165 - justify-content: center; 166 - gap: 0.5rem; 167 - padding: 1rem; 168 - flex: 1; 169 - background: hsla(var(--surface1) / 0.1); 170 - border-radius: var(--radius-md); 171 - text-decoration: none; 172 - color: hsl(var(--text)); 173 - 174 - .action-icon { 175 - font-size: 1.25rem; 176 - color: hsl(var(--accent)); 177 - } 178 - 179 - span { 180 - font-size: 0.8rem; 181 - font-weight: 600; 182 - } 183 - 184 - &:hover { 185 - background: hsla(var(--surface1) / 0.2); 186 - } 187 - } 188 - } */ 189 - 190 - .service-credit { 191 - font-size: 0.8rem; 192 - color: hsl(var(--subtext0)); 193 - opacity: 0.8; 194 - line-height: 1.4; 195 - } 196 123 } 197 124 </style>
+282
src/components/Modals/Plurality/SystemMember.vue
··· 1 + <script setup lang="ts"> 2 + import { computed } from 'vue' 3 + import Modal from '@/components/UI/BaseModal.vue' 4 + import Button from '@/components/UI/BaseButton.vue' 5 + import RichText from '@/components/RichText.vue' 6 + import AppLink from '@/components/Navigation/AppLink.vue' 7 + import { useToastStore } from '@/stores/toast' 8 + import type { HostPluralSystemMember } from '@/lex' 9 + 10 + const props = defineProps<{ 11 + member: HostPluralSystemMember.Main 12 + avatarUrl?: string 13 + }>() 14 + 15 + const toast = useToastStore() 16 + 17 + const displayName = computed(() => props.member.displayName || props.member.name || 'Member') 18 + const pronouns = computed(() => props.member.pronouns || null) 19 + const bio = computed(() => props.member.bio || null) 20 + const colour = computed(() => props.member.colour || null) 21 + const did = computed(() => props.member.did || null) 22 + 23 + const createdAt = computed(() => 24 + props.member.createdAt ? new Date(props.member.createdAt).toLocaleString() : null, 25 + ) 26 + const updatedAt = computed(() => 27 + props.member.updatedAt ? new Date(props.member.updatedAt).toLocaleString() : null, 28 + ) 29 + 30 + const customFields = computed(() => props.member.customFields || []) 31 + 32 + async function copyDid() { 33 + if (!did.value) return 34 + try { 35 + await navigator.clipboard.writeText(did.value) 36 + toast.success('DID copied to clipboard') 37 + } catch (e) { 38 + toast.error('Failed to copy DID') 39 + console.error('failed to copy DID:', e) 40 + } 41 + } 42 + </script> 43 + 44 + <template> 45 + <Modal :title="displayName" width="640px" @close="$emit('close')"> 46 + <div class="member-dialog"> 47 + <div class="header"> 48 + <div class="avatar"> 49 + <img v-if="avatarUrl" :src="avatarUrl" alt="avatar" /> 50 + <div v-else class="avatar-placeholder">{{ (displayName || '?').slice(0, 2) }}</div> 51 + </div> 52 + 53 + <div class="main-info"> 54 + <div class="name-row"> 55 + <h3 class="name">{{ displayName }}</h3> 56 + <span class="pronouns">{{ pronouns }}</span> 57 + </div> 58 + 59 + <div class="dates"> 60 + <span v-if="createdAt">Joined: {{ createdAt }}</span> 61 + <span v-if="updatedAt">Updated: {{ updatedAt }}</span> 62 + </div> 63 + 64 + <div class="colour-row" v-if="colour"> 65 + <span class="colour-label">Colour</span> 66 + <span class="colour-swatch" :style="{ background: colour }" /> 67 + <small class="colour-code">{{ colour }}</small> 68 + </div> 69 + </div> 70 + </div> 71 + 72 + <div class="body"> 73 + <div v-if="bio" class="bio"> 74 + <RichText :text="bio" /> 75 + </div> 76 + 77 + <div v-if="customFields && customFields.length" class="custom-fields"> 78 + <h4>Custom fields</h4> 79 + <ul> 80 + <li v-for="(f, i) in customFields" :key="i" class="custom-field"> 81 + <div class="field-name">{{ f.name }}</div> 82 + <div class="field-value">{{ f.value }}</div> 83 + </li> 84 + </ul> 85 + </div> 86 + 87 + <div class="did-row" v-if="did"> 88 + <h4>DID</h4> 89 + <div class="did-actions"> 90 + <AppLink v-if="did" name="user-profile" :params="{ id: did }" class="did-link"> 91 + View profile 92 + </AppLink> 93 + <button class="copy-btn" @click="copyDid" type="button">Copy DID</button> 94 + <span class="did-text">{{ did }}</span> 95 + </div> 96 + </div> 97 + </div> 98 + </div> 99 + 100 + <template #footer> 101 + <Button variant="primary" @click="$emit('close')">Close</Button> 102 + </template> 103 + </Modal> 104 + </template> 105 + 106 + <style scoped lang="scss"> 107 + .member-dialog { 108 + display: flex; 109 + flex-direction: column; 110 + gap: 1rem; 111 + padding: 0.25rem 0; 112 + 113 + .header { 114 + display: flex; 115 + gap: 1rem; 116 + align-items: center; 117 + 118 + .avatar { 119 + width: 5.25rem; 120 + height: 5.25rem; 121 + border-radius: 0.75rem; 122 + overflow: hidden; 123 + flex: 0 0 auto; 124 + background: hsla(var(--surface1) / 0.6); 125 + display: flex; 126 + align-items: center; 127 + justify-content: center; 128 + 129 + img { 130 + width: 100%; 131 + height: 100%; 132 + object-fit: cover; 133 + } 134 + .avatar-placeholder { 135 + font-weight: 800; 136 + font-size: 1.25rem; 137 + color: hsl(var(--text)); 138 + } 139 + } 140 + 141 + .main-info { 142 + flex: 1; 143 + display: flex; 144 + flex-direction: column; 145 + gap: 0.25rem; 146 + 147 + .name-row { 148 + display: flex; 149 + align-items: center; 150 + gap: 0.5rem; 151 + 152 + .name { 153 + margin: 0; 154 + font-size: 1.1rem; 155 + font-weight: 900; 156 + } 157 + 158 + .pronouns { 159 + font-size: 0.75rem; 160 + padding: 0.1rem 0.5rem; 161 + font-weight: 800; 162 + border-radius: 1rem; 163 + background: hsla(var(--accent) / 0.75); 164 + color: hsl(var(--base)); 165 + } 166 + } 167 + 168 + .dates { 169 + font-size: 0.85rem; 170 + color: hsl(var(--subtext0)); 171 + display: flex; 172 + gap: 1rem; 173 + flex-wrap: wrap; 174 + } 175 + 176 + .colour-row { 177 + display: flex; 178 + align-items: center; 179 + gap: 0.5rem; 180 + margin-top: 0.35rem; 181 + 182 + .colour-label { 183 + font-size: 0.85rem; 184 + color: hsl(var(--subtext0)); 185 + } 186 + 187 + .colour-swatch { 188 + width: 1.25rem; 189 + height: 1.25rem; 190 + border-radius: 6px; 191 + border: 1px solid hsla(var(--surface2) / 0.4); 192 + box-shadow: 0 1px 0 hsla(var(--crust) / 0.03) inset; 193 + } 194 + 195 + .colour-code { 196 + font-size: 0.8rem; 197 + color: hsl(var(--subtext1)); 198 + } 199 + } 200 + } 201 + } 202 + 203 + .body { 204 + display: flex; 205 + flex-direction: column; 206 + gap: 0.75rem; 207 + 208 + .bio { 209 + color: hsl(var(--text)); 210 + } 211 + 212 + .custom-fields { 213 + h4 { 214 + margin: 0 0 0.25rem 0; 215 + font-size: 0.9rem; 216 + font-weight: 700; 217 + color: hsl(var(--text)); 218 + } 219 + 220 + .custom-field { 221 + display: flex; 222 + gap: 0.5rem; 223 + 224 + .field-name { 225 + flex: 0 0 auto; 226 + font-weight: 700; 227 + color: hsl(var(--subtext1)); 228 + min-width: 4rem; 229 + } 230 + 231 + .field-value { 232 + flex: 1; 233 + color: hsl(var(--subtext0)); 234 + flex: 1; 235 + overflow-wrap: anywhere; 236 + } 237 + } 238 + } 239 + 240 + .did-row { 241 + display: flex; 242 + flex-direction: column; 243 + gap: 0.5rem; 244 + 245 + h4 { 246 + margin: 0; 247 + font-size: 0.9rem; 248 + color: hsl(var(--subtext0)); 249 + } 250 + 251 + .did-actions { 252 + display: flex; 253 + align-items: center; 254 + gap: 0.5rem; 255 + flex-wrap: wrap; 256 + } 257 + 258 + .did-text { 259 + font-family: monospace; 260 + color: hsl(var(--subtext1)); 261 + font-size: 0.85rem; 262 + overflow-wrap: anywhere; 263 + } 264 + 265 + .copy-btn { 266 + background: none; 267 + border: 1px solid hsla(var(--surface2) / 0.4); 268 + padding: 0.25rem 0.5rem; 269 + border-radius: 6px; 270 + font-size: 0.85rem; 271 + cursor: pointer; 272 + } 273 + 274 + .did-link { 275 + text-decoration: none; 276 + color: hsl(var(--accent)); 277 + font-weight: 700; 278 + } 279 + } 280 + } 281 + } 282 + </style>
+2 -2
src/components/Navigation/PageLayout.vue
··· 121 121 h4, 122 122 h5, 123 123 h6 { 124 - color: rgb(var(--text)); 124 + color: hsl(var(--text)); 125 125 margin-bottom: 0; 126 126 } 127 127 h1 { ··· 140 140 border-radius: 1rem; 141 141 height: calc(100vh - 0.5rem); 142 142 margin: 0.25rem; 143 - border: 1px solid hsla(var(--surface2) / 0.2); 143 + border: 1px solid var(--border-colour); 144 144 } 145 145 } 146 146
+5 -2
src/components/UI/ListGroup.vue
··· 18 18 .list-header { 19 19 padding: 0 var(--space-4) var(--space-2); 20 20 margin-left: var(--space-1); 21 + &.text-caption { 22 + color: hsl(var(--accent)); 23 + } 21 24 } 25 + 22 26 .list-group { 23 27 background-color: hsl(var(--surface0)); 24 - border: 1px solid hsla(var(--surface2) / 0.5); 25 28 border-radius: var(--radius-xl); 29 + border: 2px solid var(--accented-border-colour); 26 30 overflow: hidden; 27 31 display: flex; 28 32 flex-direction: column; 29 - box-shadow: 0 4px 20px -4px hsla(var(--crust) / 0.1); 30 33 } 31 34 </style>
+6 -5
src/components/UI/ListItem.vue
··· 105 105 align-items: center; 106 106 gap: var(--space-3); 107 107 padding: var(--space-4); 108 - background: hsl(var(--surface0)); 108 + background: var(--accent-surface); 109 + 109 110 text-decoration: none; 110 111 color: inherit; 111 112 min-height: 3.5rem; ··· 118 119 right: 0; 119 120 left: 1rem; 120 121 height: 1px; 121 - background-color: hsla(var(--surface2) / 0.5); 122 + background-color: var(--border-colour); 122 123 } 123 124 124 125 &:last-child::after { ··· 129 130 .is-clickable { 130 131 cursor: pointer; 131 132 &:hover { 132 - background: hsla(var(--surface1) / 1); 133 + background: var(--accent-surface-hover); 133 134 } 134 135 &:active { 135 - background: hsla(var(--surface1) / 0.6); 136 + background: var(--accent-surface-pressed); 136 137 } 137 138 &:focus-visible { 138 139 z-index: 1; 139 - background: hsl(var(--surface1)); 140 + background: hsl(var(--mantle)); 140 141 box-shadow: inset 0 0 0 2px hsl(var(--accent)); 141 142 } 142 143 }
+1 -1
src/components/UI/ModalStack.vue
··· 15 15 :is="modal.component" 16 16 v-bind="modal.props" 17 17 :z-index="9999 + index" 18 - @close="modalStore.close()" 18 + @close="modalStore.close" 19 19 /> 20 20 </TransitionGroup> 21 21 </Teleport>
+9 -2
src/views/Profile/ProfileView.vue
··· 33 33 import RichText from '@/components/RichText.vue' 34 34 35 35 import PluralModal from '@/components/Modals/Plurality/PluralHelp.vue' 36 + import PluralMemberModal from '@/components/Modals/Plurality/SystemMember.vue' 36 37 37 38 import type { ActorIdentifier } from '@atcute/lexicons' 38 39 import AppLink from '@/components/Navigation/AppLink.vue' ··· 416 417 return null 417 418 }) 418 419 420 + const handleMemberClick = (member: HostPluralSystemMember.Main) => { 421 + modal.open(PluralMemberModal, { member, avatarUrl: memberAvatar(member) }) 422 + } 423 + 419 424 watch( 420 425 () => props.id, 421 426 () => { ··· 610 615 </div> 611 616 612 617 <div class="fronters"> 613 - <div 618 + <Button 619 + variant="text" 620 + @click="handleMemberClick(fronter)" 614 621 class="fronter" 615 622 v-for="fronter in fronters" 616 623 :key="fronter.createdAt || fronter.displayName" ··· 619 626 <img :src="memberAvatar(fronter)" alt="" /> 620 627 </div> 621 628 <div class="name">{{ fronter.displayName }}</div> 622 - </div> 629 + </Button> 623 630 </div> 624 631 </div> 625 632