Files
sky_phone/frontend/src/views/apps/FeatherApp.vue
T

4933 lines
132 KiB
Vue

<script setup lang="ts">
import {
AlignLeft,
AtSign,
Bell,
Bookmark,
Camera,
CalendarDays,
CheckCircle2,
ChevronLeft,
ChevronRight,
Eye,
EyeOff,
Feather,
Home,
ImagePlus,
Images,
KeyRound,
Mail,
MessageCircle,
PencilLine,
Plus,
Search,
Share2,
ShieldCheck,
Trash2,
UserPlus,
UserMinus,
UserRound,
UsersRound,
Video,
X,
} from 'lucide-vue-next'
import {
kBlock,
kButton,
kFab,
kGlass,
kIcon,
kLink,
kList,
kListInput,
kListItem,
kNavbar,
kNavbarBackLink,
kPage,
kPreloader,
kSearchbar,
kSegmented,
kSegmentedButton,
kSheet,
kTabbar,
kTabbarLink,
kToast,
kToggle,
kToolbarPane,
} from 'konsta/vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FeatherPostCard from '@/components/feather/FeatherPostCard.vue'
import { useAccountStore } from '@/stores/account'
import { useFeatherStore } from '@/stores/feather'
import { useEasyShareStore } from '@/stores/easyshare'
import type { FeatherConnectionMode } from '@/stores/feather'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { usePhoneStore } from '@/stores/phone'
import type { FeatherMedia, FeatherPost, FeatherProfile } from '@/types/feather'
import {
filterMailAddressInput,
MAIL_ADDRESS_INPUT_MAX_LENGTH,
normalizeMailAddress,
} from '@/utils/mail'
type Tab = 'home' | 'explore' | 'network' | 'activity' | 'profile'
type Screen =
| 'main'
| 'composer'
| 'thread'
| 'profile'
| 'edit'
| 'connections'
type ExploreView = 'explore' | 'trending' | 'news'
type ActivityView = 'all' | 'mentions'
type ProfileView = 'posts' | 'replies' | 'media' | 'bookmarks'
type SelectedPhoto = { id: number; url: string }
type ComposerContext = {
body: string
photos: SelectedPhoto[]
replyTo?: FeatherPost
}
type ProfileMediaContext = {
editing: { bio: string; displayName: string }
selectedPhoto: SelectedPhoto | null
}
const phone = usePhoneStore()
const account = useAccountStore()
const feather = useFeatherStore()
const messageMedia = useMessageMediaStore()
const route = useRoute()
const router = useRouter()
const tab = ref<Tab>('home')
const screen = ref<Screen>('main')
const exploreView = ref<ExploreView>('explore')
const activityView = ref<ActivityView>('all')
const profileView = ref<ProfileView>('posts')
const connectionMode = ref<FeatherConnectionMode>('followers')
const settingsOpen = ref(false)
const compactMode = ref(false)
const showSuggestions = ref(true)
const search = ref('')
const networkSearch = ref('')
const feedback = ref('')
const busy = ref(false)
const replyTo = ref<FeatherPost | null>(null)
const composerBody = ref('')
const threadReplyBody = ref('')
const threadReplyTarget = ref<FeatherPost | null>(null)
const threadReplyInput = ref<HTMLTextAreaElement | null>(null)
const photos = ref<SelectedPhoto[]>([])
const menuPost = ref<FeatherPost | null>(null)
const reportOpen = ref(false)
const reportReason = ref('spam')
const reportDetails = ref('')
const mediaPreview = ref<{ index: number; items: FeatherMedia[] } | null>(null)
const onboarding = ref({ bio: '', displayName: '', handle: '' })
const editing = ref({ bio: '', displayName: '' })
const selectedProfilePhoto = ref<SelectedPhoto | null>(null)
const authMode = ref<'login' | 'register'>('login')
const authForm = ref({ confirm: '', email: '', password: '' })
const authBusy = ref(false)
const authAttempted = ref(false)
const authError = ref('')
const authPasswordVisible = ref(false)
const composeFabColors = {
activeBgIos: 'active:!bg-[#2778dc] dark:active:!bg-[#2778dc]',
bgIos: '!bg-[#58a6ff] dark:!bg-[#58a6ff]',
textIos: '!text-white dark:!text-white',
}
let exploreSearchTimer: number | undefined
let networkSearchTimer: number | undefined
const displayedPosts = computed(() => {
if (tab.value === 'explore' && search.value.trim())
return feather.explorePosts
if (tab.value === 'profile' || screen.value === 'profile')
return feather.profilePosts
return feather.feed
})
const displayedActivities = computed(() =>
activityView.value === 'mentions'
? feather.activities.filter(
(item) => item.kind === 'reply' || item.kind === 'quote',
)
: feather.activities,
)
const displayedProfilePosts = computed(() => {
if (profileView.value === 'bookmarks') return feather.bookmarkedPosts
if (profileView.value === 'replies')
return feather.profilePosts.filter((post) => Boolean(post.reply_to_id))
if (profileView.value === 'media')
return feather.profilePosts.filter((post) => post.media.length > 0)
return feather.profilePosts.filter((post) => !post.reply_to_id)
})
const previewMedia = computed(() => {
if (!mediaPreview.value) return null
return mediaPreview.value.items[mediaPreview.value.index] ?? null
})
const networkSections = computed(() => {
const sections: Array<{
key: 'results' | 'suggestions'
profiles: FeatherProfile[]
title: string
}> = []
if (networkSearch.value.trim()) {
sections.push({
key: 'results',
profiles: feather.networkResults,
title: t('searchResults'),
})
}
sections.push({
key: 'suggestions',
profiles: feather.networkSuggestions,
title: t('suggestedPeople'),
})
return sections
})
const exploreCategories: ExploreView[] = ['explore', 'trending', 'news']
const canPost = computed(
() =>
!busy.value &&
composerBody.value.length <= 360 &&
(composerBody.value.trim().length > 0 || photos.value.length > 0),
)
const canReply = computed(() => {
const mention =
threadReplyTarget.value &&
threadReplyTarget.value.id !== feather.thread?.post.id
? `@${threadReplyTarget.value.handle}`
: ''
return (
!busy.value &&
threadReplyBody.value.trim().length > 0 &&
threadReplyBody.value.trim() !== mention &&
threadReplyBody.value.length <= 360
)
})
const canSaveProfile = computed(
() =>
!busy.value &&
editing.value.displayName.trim().length > 0 &&
editing.value.displayName.length <= 50 &&
editing.value.bio.length <= 160,
)
const editProfileAvatarUrl = computed(
() => selectedProfilePhoto.value?.url ?? feather.profile?.avatar_url,
)
const canCreateProfile = computed(
() =>
!busy.value &&
onboarding.value.displayName.trim().length > 0 &&
/^[a-z0-9][a-z0-9_]{1,28}[a-z0-9]$/i.test(onboarding.value.handle.trim()),
)
const activeProfile = computed(() => feather.viewedProfile ?? feather.profile)
const navbarTitle = computed(() => {
if (screen.value === 'composer')
return replyTo.value ? t('reply') : t('newPost')
if (screen.value === 'thread') return t('posts')
if (screen.value === 'edit') return t('editProfile')
if (screen.value === 'connections')
return connectionMode.value === 'followers'
? t('followers')
: t('followingCount')
if (screen.value === 'profile') return `@${activeProfile.value?.handle ?? ''}`
if (tab.value === 'activity') return t('activity')
if (tab.value === 'network') return t('network')
if (tab.value === 'profile')
return activeProfile.value?.display_name ?? t('profile')
if (tab.value === 'explore') return t('explore')
return t('name')
})
const unreadActivities = computed(
() => feather.activities.filter((item) => !item.read).length,
)
const authEmailValid = computed(
() => normalizeMailAddress(authForm.value.email) !== null,
)
const authPasswordValid = computed(() => {
const length = authForm.value.password.length
return length >= 6 && length <= 64
})
const authConfirmValid = computed(
() =>
authMode.value === 'login' ||
(authForm.value.confirm.length > 0 &&
authForm.value.confirm === authForm.value.password),
)
function t(path: string, params?: Record<string, string>): string {
return phone.t(`Apps.feather.${path}`, params)
}
function openMediaPreview(post: FeatherPost, index: number): void {
mediaPreview.value = { index, items: post.media }
}
function closeMediaPreview(): void {
mediaPreview.value = null
}
function moveMediaPreview(direction: number): void {
if (!mediaPreview.value || mediaPreview.value.items.length < 2) return
mediaPreview.value.index =
(mediaPreview.value.index + direction + mediaPreview.value.items.length) %
mediaPreview.value.items.length
}
function handleMediaPreviewKeydown(event: KeyboardEvent): void {
if (!mediaPreview.value) return
if (event.key === 'Escape') closeMediaPreview()
if (event.key === 'ArrowLeft') moveMediaPreview(-1)
if (event.key === 'ArrowRight') moveMediaPreview(1)
}
function toast(path: string): void {
feedback.value = t(path)
window.setTimeout(() => {
feedback.value = ''
}, 2200)
}
function errorToast(error?: string): void {
const key =
error === 'invalid_handle'
? 'errors.invalidHandle'
: error === 'handle_taken'
? 'errors.handleTaken'
: error === 'invalid_post'
? 'errors.invalidPost'
: 'errors.generic'
toast(key)
}
function updateAuthEmail(event: Event): void {
const input = event.target as HTMLInputElement
const filtered = filterMailAddressInput(input.value)
if (input.value !== filtered) input.value = filtered
authForm.value.email = filtered
}
function inputValue(event: Event): string {
const target = event.target
if (
!(target instanceof HTMLInputElement) &&
!(target instanceof HTMLTextAreaElement)
) {
console.error('[Feather] Text input event has no supported target.')
return ''
}
return target.value
}
function switchAuthMode(mode: 'login' | 'register'): void {
authMode.value = mode
authForm.value.confirm = ''
authError.value = ''
authAttempted.value = false
}
function authErrorMessage(error?: string): string {
const known = [
'invalid_email',
'invalid_password',
'invalid_credentials',
'email_taken',
'rate_limited',
]
return t(`authErrors.${error && known.includes(error) ? error : 'default'}`)
}
async function submitAuth(): Promise<void> {
authAttempted.value = true
authError.value = ''
if (!authEmailValid.value) {
authError.value = t('authErrors.invalid_email')
return
}
if (!authPasswordValid.value) {
authError.value = t('authErrors.invalid_password')
return
}
if (!authConfirmValid.value) {
authError.value = t('authErrors.password_mismatch')
return
}
const email = normalizeMailAddress(authForm.value.email)
if (!email) return
authBusy.value = true
const response =
authMode.value === 'login'
? await account.login(email, authForm.value.password)
: await account.register(email, authForm.value.password)
authBusy.value = false
if (!response.success) {
authError.value = authErrorMessage(response.error)
return
}
authForm.value = { confirm: '', email: '', password: '' }
authAttempted.value = false
authPasswordVisible.value = false
await feather.bootstrap()
}
async function createProfile(): Promise<void> {
if (!canCreateProfile.value) return
busy.value = true
const response = await feather.createProfile({
bio: onboarding.value.bio,
displayName: onboarding.value.displayName,
handle: onboarding.value.handle,
})
busy.value = false
if (!response.success) errorToast(response.error)
}
async function selectTab(next: Tab): Promise<void> {
tab.value = next
screen.value = 'main'
feather.viewedProfile = null
if (next === 'home') await feather.loadFeed()
if (next === 'explore') await feather.explore(search.value)
if (next === 'network') await feather.loadNetwork(networkSearch.value)
if (next === 'activity') {
await feather.loadActivities()
await feather.markActivities()
}
if (next === 'profile' && feather.profile) {
profileView.value = 'posts'
await feather.loadProfile(feather.profile.id)
}
}
async function selectProfileView(next: ProfileView): Promise<void> {
profileView.value = next
if (next === 'bookmarks') await feather.loadBookmarks()
}
async function setFeedMode(mode: 'for-you' | 'following'): Promise<void> {
await feather.loadFeed(mode)
}
async function runSearch(): Promise<void> {
if (exploreSearchTimer !== undefined) {
window.clearTimeout(exploreSearchTimer)
exploreSearchTimer = undefined
}
await feather.explore(search.value)
}
async function runNetworkSearch(): Promise<void> {
await feather.loadNetwork(networkSearch.value)
}
async function selectTopic(topic: string): Promise<void> {
search.value = topic
exploreView.value = 'explore'
await runSearch()
}
function shareProfile(): void {
const profile = activeProfile.value
if (!profile) return
useEasyShareStore().open({
appId: 'feather',
copyText: `@${profile.handle}`,
id: profile.id,
imageUrl: profile.avatar_url,
kind: 'profile',
link: `skyphone://feather/profile/${profile.id}`,
subtitle: `@${profile.handle}`,
title: profile.display_name,
})
}
function sharePost(post: FeatherPost): void {
useEasyShareStore().open({
appId: 'feather',
copyText: `@${post.handle}: ${post.body}`,
id: post.id,
imageUrl: post.media[0]?.url,
kind: 'post',
link: `skyphone://feather/post/${post.id}`,
subtitle: `@${post.handle}`,
title: post.body,
})
}
function openComposer(post?: FeatherPost): void {
replyTo.value = post ?? null
composerBody.value = ''
photos.value = []
screen.value = 'composer'
}
function openComposerMedia(app: 'camera' | 'photos'): void {
const remaining = 4 - photos.value.length
if (remaining < 1) {
toast('photoLimit')
return
}
messageMedia.begin(
'feather:composer',
'photo',
'/apps/feather?compose=1',
app === 'photos' ? remaining : 1,
{
body: composerBody.value,
photos: [...photos.value],
replyTo: replyTo.value ?? undefined,
} satisfies ComposerContext,
)
void router.push({
path: `/apps/${app}`,
query: { mediaAttachment: 'photo' },
})
}
async function publish(): Promise<void> {
if (!canPost.value) return
busy.value = true
const response = await feather.createPost({
body: composerBody.value,
mediaIds: photos.value.map((photo) => photo.id),
replyToId: replyTo.value?.id,
})
busy.value = false
if (!response.success) {
errorToast(response.error)
return
}
if (replyTo.value) await feather.loadThread(replyTo.value.id)
screen.value = replyTo.value ? 'thread' : 'main'
replyTo.value = null
composerBody.value = ''
photos.value = []
}
async function openThread(
post: FeatherPost,
focusReply = false,
): Promise<void> {
if (!(await feather.loadThread(post.id))) return
threadReplyBody.value = ''
threadReplyTarget.value = feather.thread?.post ?? null
screen.value = 'thread'
if (focusReply) {
await nextTick()
threadReplyInput.value?.focus()
}
}
async function focusThreadReply(post: FeatherPost): Promise<void> {
const previousTarget = threadReplyTarget.value
const previousMention =
previousTarget && previousTarget.id !== feather.thread?.post.id
? `@${previousTarget.handle} `
: ''
const nextMention =
post.id !== feather.thread?.post.id ? `@${post.handle} ` : ''
const bodyWithoutPreviousMention = previousMention
? threadReplyBody.value.replace(new RegExp(`^${previousMention}`), '')
: threadReplyBody.value
threadReplyTarget.value = post
if (!threadReplyBody.value.trim() || previousMention || nextMention) {
threadReplyBody.value = `${nextMention}${bodyWithoutPreviousMention}`
}
await nextTick()
threadReplyInput.value?.focus()
}
async function clearThreadReplyTarget(): Promise<void> {
if (!feather.thread) return
const mention = threadReplyTarget.value
? `@${threadReplyTarget.value.handle} `
: ''
if (threadReplyBody.value.startsWith(mention))
threadReplyBody.value = threadReplyBody.value.slice(mention.length)
threadReplyTarget.value = feather.thread.post
await nextTick()
threadReplyInput.value?.focus()
}
async function publishThreadReply(): Promise<void> {
if (!canReply.value || !feather.thread) return
const postId = feather.thread.post.id
busy.value = true
const response = await feather.createPost({
body: threadReplyBody.value,
mediaIds: [],
replyToId: postId,
})
busy.value = false
if (!response.success) {
errorToast(response.error)
return
}
threadReplyBody.value = ''
await feather.loadThread(postId)
threadReplyTarget.value = feather.thread?.post ?? null
await nextTick()
threadReplyInput.value?.focus()
}
async function openProfile(profileId: number): Promise<void> {
if (await feather.loadProfile(profileId)) {
profileView.value = 'posts'
screen.value = 'profile'
}
}
async function followProfile(profile: FeatherProfile): Promise<void> {
await feather.follow(profile)
}
async function openConnections(mode: FeatherConnectionMode): Promise<void> {
if (!activeProfile.value) return
connectionMode.value = mode
screen.value = 'connections'
await feather.loadConnections(activeProfile.value.id, mode)
}
async function removeConnection(profile: FeatherProfile): Promise<void> {
if (await feather.removeConnection(profile, connectionMode.value))
toast(
connectionMode.value === 'followers'
? 'followerRemoved'
: 'followingRemoved',
)
}
function openEdit(): void {
if (!feather.profile) return
editing.value = {
bio: feather.profile.bio,
displayName: feather.profile.display_name,
}
selectedProfilePhoto.value = null
screen.value = 'edit'
}
function openProfileMedia(app: 'camera' | 'photos'): void {
messageMedia.begin(
'feather:profile-avatar',
'photo',
'/apps/feather?profileEdit=1',
1,
{
editing: { ...editing.value },
selectedPhoto: selectedProfilePhoto.value,
} satisfies ProfileMediaContext,
)
void router.push({
path: `/apps/${app}`,
query: { mediaAttachment: 'photo' },
})
}
function closeProfileEdit(): void {
tab.value = 'profile'
screen.value = 'main'
feather.viewedProfile = null
selectedProfilePhoto.value = null
if (route.query.profileEdit === '1')
void router.replace({ path: '/apps/feather' })
}
async function saveProfile(): Promise<void> {
busy.value = true
const response = await feather.updateProfile({
...editing.value,
avatarId: selectedProfilePhoto.value?.id,
})
busy.value = false
if (!response.success) {
errorToast(response.error)
return
}
if (feather.profile) await feather.loadProfile(feather.profile.id)
closeProfileEdit()
}
async function deletePost(): Promise<void> {
if (!menuPost.value) return
const success = await feather.deletePost(menuPost.value.id)
menuPost.value = null
if (success) toast('deleted')
}
async function blockPostAuthor(): Promise<void> {
if (!menuPost.value) return
const success = await feather.blockProfile(menuPost.value.profile_id)
menuPost.value = null
if (success) {
screen.value = 'main'
toast('blocked')
}
}
async function submitReport(): Promise<void> {
if (!menuPost.value) return
const success = await feather.reportPost(
menuPost.value.id,
reportReason.value,
reportDetails.value,
)
reportOpen.value = false
menuPost.value = null
reportDetails.value = ''
if (success) toast('reported')
}
function goBack(): void {
if (screen.value === 'connections') {
screen.value = 'profile'
return
}
if (screen.value === 'edit') {
closeProfileEdit()
return
}
if (screen.value === 'thread' || screen.value === 'profile') {
screen.value = 'main'
feather.viewedProfile = null
return
}
if (screen.value === 'composer') {
screen.value = replyTo.value ? 'thread' : 'main'
return
}
void router.back()
}
watch(networkSearch, () => {
if (networkSearchTimer !== undefined) window.clearTimeout(networkSearchTimer)
networkSearchTimer = window.setTimeout(() => {
if (tab.value === 'network') void runNetworkSearch()
}, 250)
})
watch(
search,
(value) => {
if (exploreSearchTimer !== undefined)
window.clearTimeout(exploreSearchTimer)
if (!value.trim()) {
exploreSearchTimer = undefined
return
}
exploreSearchTimer = window.setTimeout(() => {
exploreSearchTimer = undefined
if (tab.value === 'explore') void runSearch()
}, 300)
},
{ flush: 'sync' },
)
onBeforeUnmount(() => {
if (exploreSearchTimer !== undefined) window.clearTimeout(exploreSearchTimer)
if (networkSearchTimer !== undefined) window.clearTimeout(networkSearchTimer)
window.removeEventListener('keydown', handleMediaPreviewKeydown)
})
onMounted(async () => {
window.addEventListener('keydown', handleMediaPreviewKeydown)
const selection =
messageMedia.consumeMany<ComposerContext>('feather:composer')
const profileSelection =
messageMedia.consumeMany<ProfileMediaContext>('feather:profile-avatar')
if (selection) {
if (selection.context) {
composerBody.value = selection.context.body
photos.value = selection.context.photos
replyTo.value = selection.context.replyTo ?? null
}
for (const media of selection.media) {
if (photos.value.some((photo) => photo.id === media.id)) continue
photos.value.push({ id: media.id, url: media.url })
}
}
if (profileSelection) {
if (profileSelection.context) {
editing.value = profileSelection.context.editing
selectedProfilePhoto.value = profileSelection.context.selectedPhoto
}
if (profileSelection.media[0]) {
selectedProfilePhoto.value = {
id: profileSelection.media[0].id,
url: profileSelection.media[0].url,
}
}
}
if (route.query.compose === '1') screen.value = 'composer'
await feather.bootstrap()
if (route.query.profileEdit === '1' && feather.profile) screen.value = 'edit'
if (feather.onboarded) {
const easyShareId = String(route.query.easyShareId ?? '')
if (easyShareId && route.query.easyShareKind === 'profile') {
const profileId = Number(easyShareId)
if (Number.isInteger(profileId) && profileId > 0)
await openProfile(profileId)
} else if (easyShareId && route.query.easyShareKind === 'post') {
if (await feather.loadThread(easyShareId)) {
threadReplyTarget.value = feather.thread?.post ?? null
screen.value = 'thread'
}
}
await feather.loadActivities()
if (tab.value === 'profile' && feather.profile)
await feather.loadProfile(feather.profile.id)
}
})
</script>
<template>
<kPage
component="main"
class="feather-app"
:class="{
'feather-app--active': feather.onboarded,
'feather-app--home':
feather.onboarded && screen === 'main' && tab === 'home',
'feather-app--light': feather.onboarded && !phone.isDarkMode,
'feather-app--section':
feather.onboarded && screen === 'main' && tab !== 'home',
'native-app': feather.onboarded,
'feather-app--compact': compactMode,
}"
>
<kNavbar
v-if="feather.onboarded"
class="feather-navbar"
:left-class="
screen === 'composer' ? 'feather-navbar__plain-action' : undefined
"
:right-class="
screen === 'composer' || screen === 'edit'
? 'feather-navbar__plain-action'
: undefined
"
:subtitle="screen === 'main' && tab === 'home' ? undefined : t('name')"
:title="screen === 'main' && tab === 'home' ? undefined : navbarTitle"
>
<template v-if="screen === 'main' && tab === 'home'" #title>
<Feather
class="feather-navbar__brand-mark"
:size="30"
:stroke-width="1.8"
/>
</template>
<template v-if="screen !== 'main'" #left>
<kNavbarBackLink
v-if="screen === 'profile'"
:text="t('back')"
:show-text="false"
@click="goBack"
/>
<kLink
v-else-if="screen === 'composer'"
component="button"
class="feather-composer-close"
:link-props="{ type: 'button' }"
:aria-label="t('cancel')"
@click="goBack"
>
<X :size="19" />
</kLink>
<kNavbarBackLink
v-else
:text="t('back')"
:show-text="false"
@click="goBack"
/>
</template>
<template #right>
<kButton
v-if="screen === 'composer'"
rounded
small
:disabled="!canPost"
class="feather-primary feather-composer-publish"
@click="publish"
>
{{ busy ? t('posting') : t('post') }}
</kButton>
<kButton
v-else-if="screen === 'edit'"
rounded
small
:disabled="!canSaveProfile"
class="feather-primary feather-edit__navbar-save"
@click="saveProfile"
>
{{ busy ? t('loading') : t('saveProfile') }}
</kButton>
</template>
</kNavbar>
<div v-if="feather.loading && !feather.onboarded" class="feather-loading">
<kPreloader class="text-[#438cf5]" />
<span>{{ t('loading') }}</span>
</div>
<section v-else-if="!account.email" class="feather-auth">
<header class="feather-auth__hero">
<div class="feather-welcome__mark"><Feather :size="43" /></div>
<div>
<span>{{ t('authEyebrow') }}</span>
<h1>{{ t('authWelcome') }}</h1>
<p>{{ t('authBody') }}</p>
</div>
</header>
<form class="feather-auth__card" @submit.prevent="submitAuth">
<kSegmented raised class="feather-auth__modes">
<kSegmentedButton
type="button"
:active="authMode === 'login'"
@click="switchAuthMode('login')"
>
{{ t('login') }}
</kSegmentedButton>
<kSegmentedButton
type="button"
:active="authMode === 'register'"
@click="switchAuthMode('register')"
>
{{ t('register') }}
</kSegmentedButton>
</kSegmented>
<div class="feather-auth__copy">
<h2>
{{ t(authMode === 'login' ? 'loginTitle' : 'registerTitle') }}
</h2>
<p>{{ t(authMode === 'login' ? 'loginBody' : 'registerBody') }}</p>
</div>
<kList strong inset class="feather-auth__fields">
<kListInput
class="relative"
:value="authForm.email"
:label="t('email')"
:placeholder="t('emailPlaceholder')"
:maxlength="MAIL_ADDRESS_INPUT_MAX_LENGTH"
autocomplete="username"
autocapitalize="none"
autocorrect="off"
inputmode="email"
spellcheck="false"
:input-class="!authForm.email.includes('@') ? 'pr-20' : undefined"
:error="
authAttempted && !authEmailValid
? t('authErrors.invalid_email')
: ''
"
@input="updateAuthEmail"
>
<template #media><Mail :size="19" /></template>
<span
v-if="!authForm.email.includes('@')"
class="feather-auth__domain"
>@ifruit.com</span
>
</kListInput>
<kListInput
:value="authForm.password"
class="relative"
:type="authPasswordVisible ? 'text' : 'password'"
:label="t('password')"
:placeholder="t('passwordPlaceholder')"
:maxlength="64"
:autocomplete="
authMode === 'login' ? 'current-password' : 'new-password'
"
:error="
authAttempted && !authPasswordValid
? t('authErrors.invalid_password')
: ''
"
@input="authForm.password = inputValue($event)"
>
<template #media><KeyRound :size="19" /></template>
<button
class="feather-auth__reveal"
type="button"
:aria-label="
t(authPasswordVisible ? 'hidePassword' : 'showPassword')
"
@click="authPasswordVisible = !authPasswordVisible"
>
<EyeOff v-if="authPasswordVisible" :size="18" />
<Eye v-else :size="18" />
</button>
</kListInput>
<kListInput
v-if="authMode === 'register'"
:value="authForm.confirm"
:type="authPasswordVisible ? 'text' : 'password'"
:label="t('confirmPassword')"
:placeholder="t('confirmPasswordPlaceholder')"
:maxlength="64"
autocomplete="new-password"
:error="
authAttempted && !authConfirmValid
? t('authErrors.password_mismatch')
: ''
"
@input="authForm.confirm = inputValue($event)"
>
<template #media><ShieldCheck :size="19" /></template>
</kListInput>
</kList>
<div v-if="authError" class="feather-auth__error" role="alert">
{{ authError }}
</div>
<kButton
component="button"
type="submit"
large
rounded
:disabled="authBusy"
class="feather-primary feather-auth__submit"
>
<kPreloader v-if="authBusy" />
<template v-else>
{{ t(authMode === 'login' ? 'loginAction' : 'registerAction') }}
</template>
</kButton>
<p class="feather-auth__switch">
{{ t(authMode === 'login' ? 'noAccount' : 'haveAccount') }}
<button
type="button"
@click="switchAuthMode(authMode === 'login' ? 'register' : 'login')"
>
{{ t(authMode === 'login' ? 'registerNow' : 'loginNow') }}
</button>
</p>
</form>
<div class="feather-auth__trust">
<ShieldCheck :size="16" />
<span>{{ t('authTrust') }}</span>
</div>
</section>
<section v-else-if="!feather.onboarded" class="feather-onboarding">
<div class="feather-welcome__mark"><Feather :size="45" /></div>
<span class="feather-onboarding__step">{{ t('profileStep') }}</span>
<h1>{{ t('welcome') }}</h1>
<p>{{ t('welcomeBody') }}</p>
<div class="feather-onboarding__account">
<CheckCircle2 :size="15" />
<span>{{ t('accountConnected') }}</span>
<strong>{{ account.email }}</strong>
</div>
<div class="feather-onboarding__form">
<div class="feather-onboarding__form-head">
<span><UserRound :size="20" /></span>
<div>
<h2>{{ t('createProfile') }}</h2>
<p>{{ t('profileDetailsHint') }}</p>
</div>
</div>
<kList strong inset class="feather-onboarding__fields">
<kListInput
:value="onboarding.displayName"
:label="t('displayName')"
:placeholder="t('displayNamePlaceholder')"
:maxlength="50"
@input="onboarding.displayName = inputValue($event)"
>
<template #media><UserRound :size="18" /></template>
</kListInput>
<kListInput
:value="onboarding.handle"
:label="t('handle')"
:placeholder="t('handlePlaceholder')"
:maxlength="30"
:info="t('handleHint')"
@input="onboarding.handle = inputValue($event)"
>
<template #media><AtSign :size="18" /></template>
</kListInput>
<kListInput
:value="onboarding.bio"
type="textarea"
:label="t('bio')"
:placeholder="t('bioPlaceholder')"
:maxlength="160"
:info="`${onboarding.bio.length}/160`"
@input="onboarding.bio = inputValue($event)"
>
<template #media><AlignLeft :size="18" /></template>
</kListInput>
</kList>
</div>
<kButton
large
rounded
:disabled="!canCreateProfile"
class="feather-primary feather-onboarding__button"
@click="createProfile"
>
{{ t('start') }}
</kButton>
</section>
<template v-else>
<section v-if="screen === 'composer'" class="feather-composer">
<p v-if="replyTo" class="feather-replying">
{{ t('replyingTo', { handle: replyTo.handle }) }}
</p>
<kGlass :highlight="false" class="feather-composer-card">
<div class="feather-composer-card__inner">
<div class="feather-composer__identity">
<div class="feather-compose-avatar">
<img
v-if="feather.profile?.avatar_url"
:src="feather.profile.avatar_url"
alt=""
/>
<UserRound v-else :size="20" />
</div>
<div>
<strong>{{ feather.profile?.display_name }}</strong>
<span>@{{ feather.profile?.handle }}</span>
</div>
</div>
<textarea
v-model="composerBody"
autofocus
:maxlength="360"
:placeholder="
replyTo ? t('replyPlaceholder') : t('composerPlaceholder')
"
></textarea>
<div class="feather-composer-card__footer">
<span><UsersRound :size="14" /> {{ t('composerAudience') }}</span>
<b>{{
t('charactersLeft', {
count: String(360 - composerBody.length),
})
}}</b>
</div>
</div>
</kGlass>
<section class="feather-composer-media">
<header>
<span><ImagePlus :size="21" /></span>
<div>
<strong>{{ t('mediaTitle') }}</strong>
<small>{{ t('mediaBody') }}</small>
</div>
<b>{{ photos.length }} / 4</b>
</header>
<div class="feather-composer-media__actions">
<kGlass :highlight="false">
<button
type="button"
:disabled="photos.length >= 4"
@click="openComposerMedia('photos')"
>
<span><Images :size="20" /></span>
<strong>{{ t('chooseGallery') }}</strong>
<small>{{ t('chooseGalleryBody') }}</small>
</button>
</kGlass>
<kGlass :highlight="false">
<button
type="button"
:disabled="photos.length >= 4"
@click="openComposerMedia('camera')"
>
<span><Camera :size="20" /></span>
<strong>{{ t('takePhoto') }}</strong>
<small>{{ t('takePhotoBody') }}</small>
</button>
</kGlass>
</div>
<div v-if="photos.length" class="feather-composer-media__selected">
<div class="feather-composer-media__selected-head">
<strong>{{ t('selectedPhotos') }}</strong>
<span>{{ photos.length }} / 4</span>
</div>
<div
class="feather-compose-media"
:class="`feather-compose-media--${photos.length}`"
>
<div v-for="(photo, index) in photos" :key="photo.id">
<img :src="photo.url" alt="" />
<button
type="button"
:aria-label="t('removePhoto', { number: String(index + 1) })"
@click="photos.splice(index, 1)"
>
<X :size="14" />
</button>
<i>{{ index + 1 }}</i>
</div>
</div>
</div>
</section>
</section>
<section v-else-if="screen === 'edit'" class="feather-edit">
<div class="feather-edit__identity">
<div class="feather-edit__avatar">
<img
v-if="editProfileAvatarUrl"
:src="editProfileAvatarUrl"
alt=""
/>
<UserRound v-else :size="28" />
</div>
<div class="feather-edit__account">
<strong>{{ feather.profile?.display_name }}</strong>
<span>@{{ feather.profile?.handle }}</span>
</div>
<span class="feather-edit__badge"><PencilLine :size="14" /></span>
</div>
<div class="feather-edit__photo">
<strong>{{ t('chooseAvatar') }}</strong>
<div class="feather-edit__photo-actions">
<kButton tonal rounded @click="openProfileMedia('photos')">
<Images :size="16" />
<span>{{ t('chooseGallery') }}</span>
</kButton>
<kButton tonal rounded @click="openProfileMedia('camera')">
<Camera :size="16" />
<span>{{ t('takePhoto') }}</span>
</kButton>
</div>
</div>
<kList strong inset class="feather-edit__fields">
<kListInput
:value="editing.displayName"
:label="t('displayName')"
:placeholder="t('displayNamePlaceholder')"
:maxlength="50"
:info="`${editing.displayName.length}/50`"
@input="editing.displayName = inputValue($event)"
>
<template #media><UserRound :size="18" /></template>
</kListInput>
<kListInput
:value="editing.bio"
type="textarea"
:label="t('bio')"
:placeholder="t('bioPlaceholder')"
:maxlength="160"
:info="`${editing.bio.length}/160`"
@input="editing.bio = inputValue($event)"
>
<template #media><AlignLeft :size="18" /></template>
</kListInput>
</kList>
</section>
<section
v-else-if="screen === 'connections' && activeProfile"
class="feather-scroll feather-connections"
>
<kSegmented class="feather-connections__tabs">
<kSegmentedButton
type="button"
:active="connectionMode === 'followers'"
:class="{ 'is-active': connectionMode === 'followers' }"
@click="openConnections('followers')"
>
{{ t('followers') }}
</kSegmentedButton>
<kSegmentedButton
type="button"
:active="connectionMode === 'following'"
:class="{ 'is-active': connectionMode === 'following' }"
@click="openConnections('following')"
>
{{ t('followingCount') }}
</kSegmentedButton>
</kSegmented>
<div
v-if="feather.connectionLoading"
class="feather-connections__loading"
>
<kPreloader />
</div>
<kBlock
v-else-if="!feather.connections.length"
strong
inset
class="feather-connections__empty"
>
<UsersRound :size="30" />
<strong>{{ t('noConnections') }}</strong>
<p>{{ t('noConnectionsBody') }}</p>
</kBlock>
<kList v-else strong inset class="feather-connections__list">
<kListItem
v-for="person in feather.connections"
:key="person.id"
:title="person.display_name"
:text="`@${person.handle}`"
>
<template #media>
<button
type="button"
class="feather-connections__profile"
:aria-label="person.display_name"
@click="openProfile(person.id)"
>
<span class="feather-avatar feather-connections__avatar">
<img
v-if="person.avatar_url"
:src="person.avatar_url"
alt=""
/>
<UserRound v-else :size="19" />
</span>
</button>
</template>
<template #after>
<kButton
v-if="activeProfile.is_owner"
outline
rounded
small
class="feather-connections__remove"
@click="removeConnection(person)"
>
<UserMinus :size="13" />
<span>{{ t('removeConnection') }}</span>
</kButton>
<kButton
v-else-if="!person.is_owner"
rounded
small
class="feather-follow-button"
:class="{
'feather-follow-button--pending': !person.is_following,
'feather-follow-button--following': person.is_following,
}"
@click="followProfile(person)"
>
{{ person.is_following ? t('following') : t('follow') }}
</kButton>
</template>
</kListItem>
</kList>
</section>
<section
v-else-if="screen === 'thread' && feather.thread"
class="feather-scroll"
>
<FeatherPostCard
:post="feather.thread.post"
@follow="feather.followPost"
@media="openMediaPreview"
@menu="menuPost = $event"
@open="() => undefined"
@profile="openProfile"
@react="feather.react"
@reply="focusThreadReply"
@share="sharePost"
/>
<kGlass :highlight="false" class="feather-thread-reply">
<form @submit.prevent="publishThreadReply">
<span class="feather-avatar feather-thread-reply__avatar">
<img
v-if="feather.profile?.avatar_url"
:src="feather.profile.avatar_url"
alt=""
/>
<UserRound v-else :size="19" />
</span>
<div>
<div
v-if="
threadReplyTarget &&
threadReplyTarget.id !== feather.thread.post.id
"
class="feather-thread-reply__comment-target"
>
<div>
<span class="feather-thread-reply__target">{{
t('replyingTo', { handle: threadReplyTarget.handle })
}}</span>
<p>{{ threadReplyTarget.body }}</p>
</div>
<kButton
clear
rounded
small
type="button"
:aria-label="t('cancel')"
@click="clearThreadReplyTarget"
>
<X :size="14" />
</kButton>
</div>
<span v-else class="feather-thread-reply__target">{{
t('replyingTo', { handle: feather.thread.post.handle })
}}</span>
<textarea
ref="threadReplyInput"
v-model="threadReplyBody"
:maxlength="360"
:placeholder="t('replyPlaceholder')"
></textarea>
<footer>
<span>{{
t('charactersLeft', {
count: String(360 - threadReplyBody.length),
})
}}</span>
<kButton
rounded
small
type="submit"
:disabled="!canReply"
class="feather-primary"
>
{{ busy ? t('posting') : t('reply') }}
</kButton>
</footer>
</div>
</form>
</kGlass>
<h2 class="feather-section-title feather-thread-title">
{{ t('replies') }}
<span>{{ feather.thread.replies.length }}</span>
</h2>
<div v-if="!feather.thread.replies.length" class="feather-thread-empty">
<MessageCircle :size="22" />
<span>{{ t('noReplies') }}</span>
</div>
<FeatherPostCard
v-for="post in feather.thread.replies"
:key="post.id"
:post="post"
@follow="feather.followPost"
@media="openMediaPreview"
@menu="menuPost = $event"
@open="openThread"
@profile="openProfile"
@react="feather.react"
@reply="focusThreadReply"
@share="sharePost"
/>
</section>
<section
v-else-if="
(screen === 'profile' || (screen === 'main' && tab === 'profile')) &&
activeProfile
"
class="feather-scroll feather-profile-screen"
>
<kGlass :highlight="false" class="feather-profile-glass">
<div class="feather-profile">
<div class="feather-profile__cover"></div>
<div class="feather-profile__top">
<div class="feather-profile__avatar">
<img
v-if="activeProfile.avatar_url"
:src="activeProfile.avatar_url"
alt=""
/>
<UserRound v-else :size="29" />
</div>
<kButton
v-if="!activeProfile.is_owner"
rounded
small
class="feather-follow-button"
:class="{
'feather-follow-button--pending': !activeProfile.is_following,
'feather-follow-button--following':
activeProfile.is_following,
}"
@click="followProfile(activeProfile)"
>
<kIcon v-if="!activeProfile.is_following"
><UserPlus :size="14"
/></kIcon>
{{ activeProfile.is_following ? t('following') : t('follow') }}
</kButton>
</div>
<div class="feather-profile__identity">
<h1>
{{ activeProfile.display_name }}
<CheckCircle2 v-if="activeProfile.verified" :size="15" />
</h1>
<span class="feather-profile__handle"
>@{{ activeProfile.handle }}</span
>
<p class="feather-profile__bio">
{{ activeProfile.bio || t('noBio') }}
</p>
<div class="feather-profile__joined">
<CalendarDays :size="14" />
{{ t('joinedDate') }}
</div>
</div>
<div class="feather-profile__stats">
<span>
<strong>{{ activeProfile.post_count }}</strong>
<small>{{ t('posts') }}</small>
</span>
<button type="button" @click="openConnections('following')">
<strong>{{ activeProfile.following }}</strong>
<small>{{ t('followingCount') }}</small>
</button>
<button type="button" @click="openConnections('followers')">
<strong>{{ activeProfile.followers }}</strong>
<small>{{ t('followers') }}</small>
</button>
</div>
<div v-if="activeProfile.is_owner" class="feather-profile__actions">
<kButton
outline
rounded
class="feather-profile-action"
@click="shareProfile"
>
<Share2 :size="15" />
<span>{{ t('shareProfile') }}</span>
</kButton>
<kButton
rounded
class="feather-primary feather-profile-action"
@click="openEdit"
>
<PencilLine :size="15" />
<span>{{ t('editProfile') }}</span>
</kButton>
</div>
</div>
</kGlass>
<kSegmented class="feather-profile-tabs">
<kSegmentedButton
type="button"
:active="profileView === 'posts'"
:class="{ 'is-active': profileView === 'posts' }"
@click="selectProfileView('posts')"
>
<AlignLeft :size="16" /> {{ t('posts') }}
</kSegmentedButton>
<kSegmentedButton
type="button"
:active="profileView === 'replies'"
:class="{ 'is-active': profileView === 'replies' }"
@click="selectProfileView('replies')"
>
<AtSign :size="17" />
</kSegmentedButton>
<kSegmentedButton
type="button"
:active="profileView === 'media'"
:class="{ 'is-active': profileView === 'media' }"
@click="selectProfileView('media')"
>
<Video :size="17" />
</kSegmentedButton>
<kSegmentedButton
v-if="activeProfile.is_owner"
type="button"
:active="profileView === 'bookmarks'"
:class="{ 'is-active': profileView === 'bookmarks' }"
:aria-label="t('bookmarks')"
:title="t('bookmarks')"
@click="selectProfileView('bookmarks')"
>
<Bookmark :size="17" />
</kSegmentedButton>
</kSegmented>
<div
v-if="profileView === 'bookmarks' && feather.bookmarksLoading"
class="feather-network-loading"
>
<kPreloader />
</div>
<div
v-else-if="
profileView === 'bookmarks' && !displayedProfilePosts.length
"
class="feather-empty"
>
<Bookmark :size="34" />
<h2>{{ t('noBookmarks') }}</h2>
<p>{{ t('noBookmarksBody') }}</p>
</div>
<FeatherPostCard
v-for="post in displayedProfilePosts"
v-show="profileView !== 'bookmarks' || !feather.bookmarksLoading"
:key="post.id"
:post="post"
@follow="feather.followPost"
@media="openMediaPreview"
@menu="menuPost = $event"
@open="openThread"
@profile="openProfile"
@react="feather.react"
@reply="openThread($event, true)"
@share="sharePost"
/>
<section
v-if="
activeProfile.is_owner &&
profileView === 'posts' &&
showSuggestions &&
feather.suggestions.length
"
class="feather-profile-suggestions"
>
<h2>{{ t('people') }}</h2>
<div class="feather-profile-suggestions__rail">
<article
v-for="person in feather.suggestions"
:key="person.id"
class="feather-profile-suggestion"
>
<button type="button" @click="openProfile(person.id)">
<span class="feather-avatar feather-profile-suggestion__avatar">
<img
v-if="person.avatar_url"
:src="person.avatar_url"
alt=""
/>
<UserRound v-else :size="20" />
</span>
<span class="feather-profile-suggestion__identity">
<strong>{{ person.display_name }}</strong>
<small>@{{ person.handle }}</small>
<p>{{ person.bio || t('noBio') }}</p>
</span>
</button>
<kButton
rounded
small
class="feather-follow-button"
:class="{
'feather-follow-button--pending': !person.is_following,
'feather-follow-button--following': person.is_following,
}"
@click="followProfile(person)"
>
{{ person.is_following ? t('following') : t('follow') }}
</kButton>
</article>
</div>
</section>
</section>
<section v-else-if="screen === 'main'" class="feather-main">
<div v-if="tab === 'home'" class="feather-feed-tabs">
<kSegmented class="feather-context-tabs">
<kSegmentedButton
type="button"
:active="feather.mode === 'for-you'"
:class="{ 'is-active': feather.mode === 'for-you' }"
@click="setFeedMode('for-you')"
>{{ t('forYou') }}</kSegmentedButton
>
<kSegmentedButton
type="button"
:active="feather.mode === 'following'"
:class="{ 'is-active': feather.mode === 'following' }"
@click="setFeedMode('following')"
>{{ t('following') }}</kSegmentedButton
>
</kSegmented>
<kButton
rounded
small
class="feather-feed-add"
@click="selectTab('network')"
>
<UsersRound :size="15" />
{{ t('add') }}
</kButton>
</div>
<div v-if="tab === 'explore'" class="feather-explore-head">
<kSearchbar
:value="search"
class="feather-explore-search"
:placeholder="t('postSearchPlaceholder')"
@clear="search = ''"
@input="search = inputValue($event)"
@keyup.enter="runSearch"
/>
<kSegmented class="feather-context-tabs feather-explore-tabs">
<kSegmentedButton
v-for="category in exploreCategories"
:key="category"
type="button"
:active="exploreView === category"
:class="{ 'is-active': exploreView === category }"
@click="exploreView = category"
>
{{ t(`exploreTabs.${category}`) }}
</kSegmentedButton>
</kSegmented>
</div>
<div v-if="tab === 'activity'" class="feather-activity-head">
<kSegmented class="feather-context-tabs">
<kSegmentedButton
type="button"
:active="activityView === 'all'"
:class="{ 'is-active': activityView === 'all' }"
@click="activityView = 'all'"
>
{{ t('all') }}
</kSegmentedButton>
<kSegmentedButton
type="button"
:active="activityView === 'mentions'"
:class="{ 'is-active': activityView === 'mentions' }"
@click="activityView = 'mentions'"
>
{{ t('mentions') }}
</kSegmentedButton>
</kSegmented>
</div>
<div v-if="tab === 'network'" class="feather-scroll feather-network">
<kGlass :highlight="false" class="feather-network-glass">
<header class="feather-network__hero">
<div>
<small>{{ t('network') }}</small>
<h1>{{ t('networkTitle') }}</h1>
<p>{{ t('networkBody') }}</p>
</div>
<UsersRound :size="35" />
</header>
</kGlass>
<kSearchbar
:value="networkSearch"
class="feather-network-search"
:placeholder="t('networkSearchPlaceholder')"
@clear="networkSearch = ''"
@input="networkSearch = inputValue($event)"
@keyup.enter="runNetworkSearch"
/>
<div v-if="feather.networkLoading" class="feather-network-loading">
<kPreloader />
</div>
<template v-else>
<section
v-for="section in networkSections"
:key="section.key"
class="feather-network-section"
>
<h2 class="feather-section-title">{{ section.title }}</h2>
<kBlock
v-if="!section.profiles.length"
strong
inset
class="feather-network-empty"
>
<Search :size="25" />
<strong>{{
section.key === 'results'
? t('noPeopleFound')
: t('noSuggestions')
}}</strong>
<p>
{{
section.key === 'results'
? t('noPeopleFoundBody')
: t('noSuggestionsBody')
}}
</p>
</kBlock>
<kList v-else strong inset class="feather-network-list">
<kListItem
v-for="person in section.profiles"
:key="person.id"
class="feather-network-person"
content-class="feather-network-person__content"
inner-class="feather-network-person__inner"
media-class="feather-network-person__media"
title-wrap-class="feather-network-person__title-wrap"
>
<template #media>
<button
type="button"
class="feather-network-person__avatar"
:aria-label="person.display_name"
@click="openProfile(person.id)"
>
<span class="feather-avatar">
<img
v-if="person.avatar_url"
:src="person.avatar_url"
alt=""
/>
<UserRound v-else :size="20" />
</span>
</button>
</template>
<template #title>
<button
type="button"
class="feather-network-person__name"
@click="openProfile(person.id)"
>
<span>{{ person.display_name }}</span>
<CheckCircle2
v-if="person.verified"
:size="13"
:aria-label="t('verified')"
/>
</button>
</template>
<template #subtitle
><span class="feather-network-person__handle"
>@{{ person.handle }}</span
></template
>
<template #text
><span class="feather-network-person__bio">{{
person.bio || t('noBio')
}}</span></template
>
<template #footer>
<span class="feather-network-person__stats">
<span
><strong>{{ person.followers }}</strong>
{{ t('followers') }}</span
>
<i></i>
<span>{{
t('postsCount', { count: String(person.post_count) })
}}</span>
</span>
</template>
<template #after>
<kButton
rounded
small
class="feather-follow-button feather-network-person__follow"
:class="{
'feather-follow-button--pending': !person.is_following,
'feather-follow-button--following': person.is_following,
}"
@click="followProfile(person)"
>
<kIcon v-if="!person.is_following"
><UserPlus :size="13"
/></kIcon>
{{ person.is_following ? t('following') : t('follow') }}
</kButton>
</template>
</kListItem>
</kList>
</section>
</template>
</div>
<div
v-else-if="tab === 'explore' && !search.trim()"
class="feather-scroll feather-trends"
>
<div v-if="!feather.topics.length" class="feather-empty">
<Search :size="34" />
<h2>{{ t('noTrendingHashtags') }}</h2>
<p>{{ t('noTrendingHashtagsBody') }}</p>
</div>
<button
v-for="topic in feather.topics"
:key="topic.tag"
type="button"
class="feather-trend"
@click="selectTopic(topic.tag)"
>
<span>{{ t(`trendKinds.${exploreView}`) }}</span>
<strong>{{ topic.tag }}</strong>
<small>{{ t('postsCount', { count: String(topic.count) }) }}</small>
<b>•••</b>
</button>
</div>
<div v-else-if="tab === 'activity'" class="feather-scroll">
<div v-if="!displayedActivities.length" class="feather-empty">
<Bell :size="34" />
<h2>{{ t('noActivity') }}</h2>
<p>{{ t('activityBody') }}</p>
</div>
<button
v-for="item in displayedActivities"
:key="item.id"
class="feather-activity"
type="button"
@click="
item.post_id
? feather
.loadThread(item.post_id)
.then((ok) => ok && (screen = 'thread'))
: openProfile(item.profile_id)
"
>
<div class="feather-avatar">
<img v-if="item.avatar_url" :src="item.avatar_url" alt="" />
<UserRound v-else :size="20" />
</div>
<p>
<strong>{{ item.display_name }}</strong>
{{ t(`activityKinds.${item.kind}`) }}
</p>
</button>
</div>
<div v-else class="feather-scroll">
<div
v-if="tab === 'explore' && feather.exploreLoading"
class="feather-network-loading"
>
<kPreloader />
</div>
<div
v-else-if="!displayedPosts.length && !feather.loading"
class="feather-empty"
>
<Search v-if="tab === 'explore'" :size="34" />
<Feather v-else :size="34" />
<h2>
{{ tab === 'explore' ? t('emptyExplore') : t('emptyFeed') }}
</h2>
<p>
{{
tab === 'explore' ? t('emptyExploreBody') : t('emptyFeedBody')
}}
</p>
</div>
<FeatherPostCard
v-for="post in displayedPosts"
v-show="tab !== 'explore' || !feather.exploreLoading"
:key="post.id"
:post="post"
@follow="feather.followPost"
@media="openMediaPreview"
@menu="menuPost = $event"
@open="openThread"
@profile="openProfile"
@react="feather.react"
@reply="openThread($event, true)"
@share="sharePost"
/>
<template v-if="tab === 'explore' && feather.suggestions.length">
<h2 class="feather-section-title">{{ t('people') }}</h2>
<div
v-for="person in feather.suggestions"
:key="person.id"
class="feather-person"
>
<div class="feather-avatar">
<img
v-if="person.avatar_url"
:src="person.avatar_url"
alt=""
/><UserRound v-else :size="20" />
</div>
<button type="button" @click="openProfile(person.id)">
<strong>{{ person.display_name }}</strong
><span>@{{ person.handle }}</span>
</button>
<kButton
rounded
small
class="feather-follow-button"
:class="{
'feather-follow-button--pending': !person.is_following,
'feather-follow-button--following': person.is_following,
}"
@click="followProfile(person)"
>
<kIcon v-if="!person.is_following"
><UserPlus :size="14"
/></kIcon>
{{ person.is_following ? t('following') : t('follow') }}
</kButton>
</div>
</template>
</div>
</section>
<kTabbar
v-if="screen === 'main'"
component="nav"
icons
labels
class="bottom-0 left-0 fixed"
inner-class="!w-full !max-w-none !gap-0 !px-1"
:aria-label="t('name')"
>
<kToolbarPane class="feather-tab-pane">
<kTabbarLink
component="button"
:active="tab === 'home'"
:link-props="{ class: 'feather-tab-button', type: 'button' }"
@click="selectTab('home')"
>
<template #label
><span class="feather-tab-label">{{ t('home') }}</span></template
>
<template #icon
><kIcon
><Home
:size="20"
:fill="tab === 'home' ? 'currentColor' : 'none'" /></kIcon
></template>
</kTabbarLink>
<kTabbarLink
component="button"
:active="tab === 'explore'"
:link-props="{ class: 'feather-tab-button', type: 'button' }"
@click="selectTab('explore')"
>
<template #label
><span class="feather-tab-label">{{
t('explore')
}}</span></template
>
<template #icon
><kIcon><Search :size="20" /></kIcon
></template>
</kTabbarLink>
<kTabbarLink
component="button"
:active="tab === 'network'"
:link-props="{ class: 'feather-tab-button', type: 'button' }"
@click="selectTab('network')"
>
<template #label
><span class="feather-tab-label">{{
t('network')
}}</span></template
>
<template #icon>
<kIcon
><UsersRound
:size="20"
:fill="tab === 'network' ? 'currentColor' : 'none'"
/></kIcon>
</template>
</kTabbarLink>
<kTabbarLink
component="button"
:active="tab === 'activity'"
:link-props="{ class: 'feather-tab-button', type: 'button' }"
@click="selectTab('activity')"
>
<template #label
><span class="feather-tab-label">{{
t('activityNav')
}}</span></template
>
<template #icon>
<span class="feather-tab-icon">
<kIcon
><Bell
:size="20"
:fill="tab === 'activity' ? 'currentColor' : 'none'"
/></kIcon>
<b v-if="unreadActivities">{{ unreadActivities }}</b>
</span>
</template>
</kTabbarLink>
<kTabbarLink
component="button"
:active="tab === 'profile'"
:link-props="{ class: 'feather-tab-button', type: 'button' }"
@click="selectTab('profile')"
>
<template #label
><span class="feather-tab-label">{{
t('profile')
}}</span></template
>
<template #icon
><kIcon
><UserRound
:size="20"
:fill="tab === 'profile' ? 'currentColor' : 'none'" /></kIcon
></template>
</kTabbarLink>
</kToolbarPane>
</kTabbar>
<kFab
v-if="screen === 'main'"
component="button"
type="button"
class="feather-compose-fab"
:colors="composeFabColors"
:aria-label="t('newPost')"
@click="openComposer()"
>
<template #icon><Plus :size="20" /></template>
</kFab>
</template>
<kSheet :opened="settingsOpen" @backdropclick="settingsOpen = false">
<kBlock strong inset class="feather-settings-sheet">
<header>
<div>
<span>{{ t('settingsEyebrow') }}</span>
<h2>{{ t('settings') }}</h2>
</div>
<kButton clear rounded @click="settingsOpen = false">
{{ t('done') }}
</kButton>
</header>
<kList strong inset>
<kListItem :title="t('compactMode')" :text="t('compactModeBody')">
<template #after>
<kToggle
:checked="compactMode"
:aria-label="t('compactMode')"
@change="compactMode = !compactMode"
/>
</template>
</kListItem>
<kListItem
:title="t('showSuggestions')"
:text="t('showSuggestionsBody')"
>
<template #after>
<kToggle
:checked="showSuggestions"
:aria-label="t('showSuggestions')"
@change="showSuggestions = !showSuggestions"
/>
</template>
</kListItem>
</kList>
</kBlock>
</kSheet>
<kSheet :opened="menuPost !== null" @backdropclick="menuPost = null">
<kBlock strong inset class="feather-action-sheet">
<kButton v-if="menuPost?.is_owner" large tonal @click="deletePost"
><Trash2 :size="18" /> {{ t('delete') }}</kButton
>
<template v-else>
<kButton large tonal @click="reportOpen = true">{{
t('report')
}}</kButton>
<kButton large tonal @click="blockPostAuthor">{{
t('block', { handle: menuPost?.handle ?? '' })
}}</kButton>
</template>
<kButton large clear @click="menuPost = null">{{
t('cancel')
}}</kButton>
</kBlock>
</kSheet>
<kSheet :opened="reportOpen" @backdropclick="reportOpen = false">
<kBlock strong inset class="feather-report">
<h2>{{ t('reportTitle') }}</h2>
<p>{{ t('reportBody') }}</p>
<select v-model="reportReason">
<option
v-for="reason in [
'spam',
'harassment',
'dangerous',
'illegal',
'other',
]"
:key="reason"
:value="reason"
>
{{ t(`reasons.${reason}`) }}
</option>
</select>
<textarea
v-model="reportDetails"
maxlength="500"
:placeholder="t('bioPlaceholder')"
></textarea>
<kButton large rounded class="feather-primary" @click="submitReport">{{
t('reportSubmit')
}}</kButton>
<kButton large clear @click="reportOpen = false">
{{ t('cancel') }}
</kButton>
</kBlock>
</kSheet>
<div
v-if="mediaPreview && previewMedia"
class="feather-media-preview"
role="dialog"
aria-modal="true"
:aria-label="t('imagePreview')"
@click="closeMediaPreview"
>
<img
:src="previewMedia.url"
:alt="t('postImage', { number: String(mediaPreview.index + 1) })"
@click.stop
/>
<template v-if="mediaPreview.items.length > 1">
<kButton
clear
rounded
class="feather-media-preview__arrow feather-media-preview__arrow--left"
:aria-label="t('previousImage')"
@click.stop="moveMediaPreview(-1)"
>
<ChevronLeft :size="20" :stroke-width="2.8" />
</kButton>
<kButton
clear
rounded
class="feather-media-preview__arrow feather-media-preview__arrow--right"
:aria-label="t('nextImage')"
@click.stop="moveMediaPreview(1)"
>
<ChevronRight :size="20" :stroke-width="2.8" />
</kButton>
<span class="feather-media-preview__count">
{{ mediaPreview.index + 1 }} / {{ mediaPreview.items.length }}
</span>
</template>
</div>
<kToast :opened="Boolean(feedback)">{{ feedback }}</kToast>
</kPage>
</template>
<style scoped>
.feather-app {
--feather-blue: #438cf5;
--feather-blue-dark: #2867d8;
background: #fff;
color: #111923;
}
.feather-media-preview {
position: absolute;
z-index: 120;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 64px 12px 42px;
background: rgb(0 0 0 / 92%);
cursor: zoom-out;
}
.feather-media-preview > img {
display: block;
max-width: 100%;
max-height: 100%;
border-radius: 12px;
object-fit: contain;
box-shadow: 0 18px 50px rgb(0 0 0 / 55%);
cursor: default;
}
.feather-media-preview__arrow {
--k-button-bg-color: rgb(10 14 20 / 88%);
--k-button-text-color: #fff;
position: absolute;
top: 50%;
width: 34px !important;
min-width: 34px;
height: 34px;
min-height: 34px;
border: 1.5px solid rgb(255 255 255 / 58%);
padding: 0;
color: #fff !important;
background: rgb(10 14 20 / 88%) !important;
box-shadow:
0 5px 16px rgb(0 0 0 / 58%),
inset 0 0 0 1px rgb(255 255 255 / 8%);
backdrop-filter: blur(10px);
transform: translateY(-50%);
}
.feather-media-preview__arrow--left {
left: 10px;
}
.feather-media-preview__arrow--right {
right: 10px;
}
.feather-media-preview__count {
position: absolute;
bottom: 48px;
left: 50%;
border: 1px solid rgb(255 255 255 / 14%);
border-radius: 999px;
padding: 5px 10px;
color: #fff;
background: rgb(22 27 34 / 78%);
backdrop-filter: blur(14px);
font-size: 11px;
font-weight: 700;
transform: translateX(-50%);
}
:global(.dark) .feather-app {
background: #090d12;
color: #f4f7fa;
}
.feather-navbar {
--k-navbar-bg-color: color-mix(in srgb, #fff 91%, transparent);
--k-safe-area-top: 46px;
flex: 0 0 auto;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
backdrop-filter: blur(18px);
}
:global(.dark) .feather-navbar {
--k-navbar-bg-color: color-mix(in srgb, #090d12 91%, transparent);
}
.feather-explore-search {
width: auto;
margin: 10px 12px 8px;
}
.feather-explore-search :deep(form) {
min-height: 32px;
}
.feather-primary {
--k-button-bg-color: var(--feather-blue);
--k-button-text-color: #fff;
}
.feather-loading,
.feather-welcome {
display: flex;
min-height: 75%;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 13px;
padding: 30px;
text-align: center;
}
.feather-loading {
flex-direction: row;
font-size: 13px;
}
.feather-welcome h1,
.feather-onboarding h1 {
margin: 4px 0 0;
font-size: 27px;
letter-spacing: -0.8px;
}
.feather-welcome p,
.feather-onboarding > p {
max-width: 290px;
margin: 0 0 12px;
color: #738092;
font-size: 13px;
line-height: 1.45;
}
.feather-welcome__mark {
display: grid;
place-items: center;
width: 86px;
height: 86px;
border-radius: 27px;
color: #fff;
background: linear-gradient(145deg, #76d0ff, #438cf5 55%, #2757d8);
box-shadow: 0 15px 35px rgb(45 111 224 / 25%);
}
.feather-auth {
min-height: 100%;
overflow-y: auto;
padding: 25px 15px 30px;
background:
radial-gradient(circle at 85% 2%, rgb(90 183 255 / 19%), transparent 34%),
radial-gradient(circle at 0 37%, rgb(67 140 245 / 9%), transparent 38%);
}
.feather-auth__hero {
display: flex;
align-items: center;
gap: 13px;
padding: 4px 3px 18px;
}
.feather-auth__hero .feather-welcome__mark {
width: 64px;
height: 64px;
flex: 0 0 64px;
border-radius: 21px;
box-shadow: 0 12px 27px rgb(45 111 224 / 24%);
}
.feather-auth__hero span {
color: var(--feather-blue);
font-size: 9px;
font-weight: 800;
letter-spacing: 1.2px;
text-transform: uppercase;
}
.feather-auth__hero h1 {
margin: 2px 0 1px;
font-size: 24px;
letter-spacing: -0.7px;
}
.feather-auth__hero p {
margin: 0;
color: #738092;
font-size: 10px;
line-height: 1.35;
}
.feather-auth__card {
padding: 13px;
border: 0.5px solid rgb(67 140 245 / 18%);
border-radius: 22px;
background: rgb(255 255 255 / 86%);
box-shadow: 0 18px 42px rgb(31 66 120 / 12%);
backdrop-filter: blur(20px);
}
:global(.dark) .feather-auth__card {
border-color: rgb(118 192 255 / 16%);
background: rgb(18 25 34 / 88%);
box-shadow: 0 18px 42px rgb(0 0 0 / 25%);
}
.feather-auth__modes {
margin-bottom: 14px;
}
.feather-auth__modes :deep(.k-segmented-button-active) {
color: var(--feather-blue);
}
.feather-auth__copy {
padding: 0 3px 8px;
}
.feather-auth__copy h2 {
margin: 0 0 3px;
font-size: 17px;
letter-spacing: -0.25px;
}
.feather-auth__copy p {
margin: 0;
color: #788493;
font-size: 10px;
line-height: 1.4;
}
.feather-auth__fields {
margin: 4px 0 10px;
}
.feather-auth__fields :deep(.k-list-item-media) {
color: var(--feather-blue);
}
.feather-auth__domain,
.feather-auth__reveal {
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
}
.feather-auth__domain {
pointer-events: none;
color: #7b8796;
font-size: 11px;
}
.feather-auth__reveal {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: 0;
border-radius: 50%;
color: #788493;
background: transparent;
}
.feather-auth__reveal:active {
background: rgb(67 140 245 / 10%);
}
.feather-auth__error {
margin: 0 4px 10px;
border-radius: 10px;
padding: 8px 10px;
color: #c43d52;
background: rgb(240 79 101 / 10%);
font-size: 10px;
line-height: 1.35;
}
.feather-auth__submit {
width: 100%;
min-height: 43px;
box-shadow: 0 9px 20px rgb(67 140 245 / 25%);
}
.feather-auth__switch {
margin: 12px 0 1px;
color: #7a8695;
font-size: 10px;
text-align: center;
}
.feather-auth__switch button {
border: 0;
padding: 0 2px;
color: var(--feather-blue);
background: transparent;
font: inherit;
font-weight: 750;
}
.feather-auth__trust {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 13px 15px 0;
color: #7b8796;
font-size: 9px;
line-height: 1.35;
text-align: center;
}
.feather-auth__trust svg {
flex: 0 0 auto;
color: #39a87b;
}
.feather-onboarding {
min-height: 100%;
overflow-y: auto;
padding: 55px 14px 30px;
text-align: center;
}
.feather-onboarding .feather-welcome__mark {
width: 80px;
height: 80px;
margin: 0 auto;
border-radius: 25px;
box-shadow: 0 14px 34px rgb(45 111 224 / 28%);
}
.feather-onboarding h1 {
margin-top: 7px;
font-size: 30px;
letter-spacing: -0.9px;
}
.feather-onboarding > p {
max-width: 310px;
margin-bottom: 14px;
font-size: 14px;
line-height: 1.5;
}
.feather-onboarding__button {
width: calc(100% - 20px);
min-height: 46px;
margin: 17px 10px 0;
font-size: 14px;
font-weight: 750;
box-shadow: 0 10px 24px rgb(67 140 245 / 24%);
}
.feather-onboarding__button:disabled {
--k-button-bg-color: rgb(67 140 245 / 13%);
--k-button-text-color: #71839a;
color: #71839a !important;
background: rgb(67 140 245 / 13%) !important;
opacity: 1;
box-shadow: none;
}
.feather-onboarding__step {
display: inline-block;
margin-top: 15px;
color: var(--feather-blue);
font-size: 11px;
font-weight: 800;
letter-spacing: 1.15px;
text-transform: uppercase;
}
.feather-onboarding__account {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 2px 9px;
margin: 3px 10px 15px;
border: 1px solid rgb(44 155 109 / 12%);
border-radius: 15px;
padding: 11px 13px;
color: #2c9b6d;
background: rgb(44 155 109 / 9%);
text-align: left;
}
.feather-onboarding__account svg {
grid-row: span 2;
}
.feather-onboarding__account span {
font-size: 11px;
font-weight: 750;
}
.feather-onboarding__account strong {
overflow: hidden;
color: #6f7c8b;
font-size: 10.5px;
font-weight: 500;
text-overflow: ellipsis;
}
.feather-onboarding__form {
margin: 0 2px;
border: 1px solid rgb(67 140 245 / 13%);
border-radius: 23px;
padding: 13px 10px 10px;
background: rgb(67 140 245 / 4%);
box-shadow: 0 14px 34px rgb(31 66 120 / 9%);
text-align: left;
}
.feather-onboarding__form-head {
display: flex;
align-items: center;
gap: 11px;
padding: 0 8px 9px;
}
.feather-onboarding__form-head > span {
display: grid;
width: 38px;
height: 38px;
flex: 0 0 38px;
place-items: center;
border-radius: 13px;
color: var(--feather-blue);
background: rgb(67 140 245 / 12%);
}
.feather-onboarding__form-head h2 {
margin: 0 0 2px;
font-size: 15px;
letter-spacing: -0.2px;
}
.feather-onboarding__form-head p {
margin: 0;
color: #738092;
font-size: 11px;
line-height: 1.35;
}
.feather-onboarding__fields {
margin: 0;
}
.feather-onboarding__fields :deep(.k-list-item-media) {
color: #72b5ff;
}
.feather-onboarding__fields :deep(.k-list-input .text-xs) {
font-size: 12px;
line-height: 1.35;
}
.feather-onboarding__fields :deep(input),
.feather-onboarding__fields :deep(textarea) {
font-size: 15px;
line-height: 1.4;
}
.feather-onboarding__fields :deep(textarea) {
min-height: 54px;
resize: none;
}
.feather-main {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
padding-bottom: 56px;
}
.feather-scroll {
min-height: 0;
flex: 1;
overflow-y: auto;
}
.feather-feed-tabs {
padding: 0;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 11%, transparent);
}
.feather-explore-head {
padding: 0;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 11%, transparent);
}
.feather-activity-head {
border-bottom: 0.5px solid color-mix(in srgb, currentColor 11%, transparent);
}
.feather-context-tabs,
.feather-profile-tabs {
background: transparent;
}
.feather-context-tabs :deep(.k-button),
.feather-profile-tabs :deep(.k-button) {
min-height: 41px;
overflow: visible;
border-radius: 0;
color: #6f7b89;
background: transparent;
font-size: 12px;
font-weight: 650;
}
.feather-context-tabs :deep(.k-button.is-active),
.feather-profile-tabs :deep(.k-button.is-active) {
color: currentColor;
font-weight: 750;
}
.feather-context-tabs :deep(.k-button.is-active)::after,
.feather-profile-tabs :deep(.k-button.is-active)::after {
position: absolute;
right: 22%;
bottom: 0;
left: 22%;
height: 3px;
border-radius: 3px 3px 0 0;
background: var(--feather-blue);
content: '';
}
.feather-trends__header {
padding: 17px 14px 9px;
}
.feather-trends__header span {
color: #738091;
font-size: 11px;
}
.feather-trends__header h2 {
margin: 2px 0 0;
font-size: 20px;
letter-spacing: -0.5px;
}
.feather-trend {
display: flex;
width: 100%;
flex-direction: column;
border: 0;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 11%, transparent);
padding: 12px 14px;
color: inherit;
background: transparent;
text-align: left;
}
.feather-trend span,
.feather-trend small {
color: #778391;
font-size: 10px;
}
.feather-trend strong {
margin: 2px 0;
font-size: 14px;
}
.feather-trend:active {
background: color-mix(in srgb, currentColor 5%, transparent);
}
.feather-empty {
display: flex;
align-items: center;
flex-direction: column;
padding: 65px 35px;
color: #8290a1;
text-align: center;
}
.feather-empty h2 {
margin: 13px 0 4px;
color: inherit;
font-size: 16px;
}
.feather-empty p {
margin: 0;
font-size: 12px;
line-height: 1.4;
}
.feather-tab-icon {
position: relative;
}
.feather-tab-icon b {
position: absolute;
top: -6px;
right: -8px;
min-width: 14px;
border-radius: 8px;
padding: 1px 3px;
color: #fff;
background: #f04f65;
font-size: 8px;
}
.feather-composer {
min-height: calc(100% - 44px);
padding: 12px 14px;
}
.feather-replying {
margin: 2px 0 10px 48px;
color: var(--feather-blue);
font-size: 11px;
}
.feather-composer__row {
display: flex;
gap: 10px;
}
.feather-compose-avatar {
display: grid;
place-items: center;
width: 42px;
height: 42px;
flex: 0 0 42px;
overflow: hidden;
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, #71c8ff, #377be7);
}
.feather-compose-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.feather-composer textarea {
width: 100%;
min-height: 165px;
resize: none;
border: 0;
padding: 8px 0;
outline: none;
color: inherit;
background: transparent;
font: inherit;
font-size: 17px;
line-height: 1.4;
}
.feather-compose-media {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
margin: 5px 0 8px 52px;
}
.feather-compose-media > div {
position: relative;
overflow: hidden;
border-radius: 12px;
}
.feather-compose-media img {
width: 100%;
height: 115px;
object-fit: cover;
}
.feather-compose-media button {
position: absolute;
top: 5px;
right: 5px;
display: grid;
place-items: center;
width: 24px;
height: 24px;
border: 0;
border-radius: 50%;
color: #fff;
background: rgb(0 0 0 / 65%);
}
.feather-composer__tools {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 52px;
border-top: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
color: #7e8996;
font-size: 10px;
}
.feather-composer__tools :deep(.k-button) {
color: var(--feather-blue);
font-size: 11px;
}
.feather-section-title {
margin: 0;
padding: 13px 14px 8px;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
font-size: 14px;
}
.feather-profile__cover {
height: 104px;
background: linear-gradient(135deg, #bceaff, #69baf8 53%, #3a78e5);
}
.feather-profile {
border-bottom: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
}
.feather-profile__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
height: 50px;
padding: 0 14px;
}
.feather-profile__top :deep(.k-button) {
margin-top: 9px;
}
.feather-profile__avatar {
display: grid;
place-items: center;
width: 76px;
height: 76px;
flex: 0 0 76px;
overflow: hidden;
transform: translateY(-38px);
border: 3px solid #fff;
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, #71c8ff, #377be7);
}
:global(.dark) .feather-profile__avatar {
border-color: #090d12;
}
.feather-profile__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.feather-profile__identity h1 {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-size: 20px;
letter-spacing: -0.45px;
}
.feather-profile__identity h1 svg {
color: var(--feather-blue);
}
.feather-profile__identity {
padding: 3px 14px 0;
}
.feather-profile__handle {
display: block;
margin-top: 1px;
color: #7c8795;
font-size: 12px;
}
.feather-profile__bio {
margin: 10px 0 8px;
font-size: 13px;
line-height: 1.45;
}
.feather-profile__stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
padding: 0 14px 13px;
color: #7c8795;
font-size: 12px;
}
.feather-profile__stats span,
.feather-profile__stats button {
display: flex;
min-width: 0;
flex-direction: column;
align-items: center;
border: 0;
border-radius: 11px;
padding: 8px 4px;
color: inherit;
background: color-mix(in srgb, currentColor 7%, transparent);
font: inherit;
text-align: center;
}
.feather-profile__stats button {
cursor: pointer;
transition:
color 150ms ease,
transform 150ms ease,
background-color 150ms ease;
}
.feather-profile__stats button:active {
transform: scale(0.96);
color: var(--feather-blue);
background: color-mix(in srgb, var(--feather-blue) 13%, transparent);
}
.feather-profile__stats strong {
color: currentColor;
font-size: 15px;
line-height: 18px;
}
.feather-profile__stats small {
max-width: 100%;
overflow: hidden;
font-size: 9.5px;
line-height: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-profile-tabs {
border-bottom: 0.5px solid color-mix(in srgb, currentColor 11%, transparent);
}
.feather-profile__avatar--edit {
margin: 60px auto 15px;
transform: none;
}
.feather-activity,
.feather-person {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
border: 0;
border-bottom: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
padding: 12px 14px;
color: inherit;
background: transparent;
text-align: left;
}
.feather-avatar {
display: grid;
place-items: center;
width: 40px;
height: 40px;
flex: 0 0 40px;
overflow: hidden;
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, #71c8ff, #377be7);
}
.feather-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.feather-activity p {
margin: 0;
font-size: 13px;
line-height: 1.4;
}
.feather-person > button {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
border: 0;
color: inherit;
background: transparent;
text-align: left;
}
.feather-person span {
color: #7c8795;
font-size: 11.5px;
}
.feather-person strong {
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-action-sheet,
.feather-report {
display: grid;
gap: 8px;
padding: 14px;
}
.feather-report h2 {
margin: 0;
font-size: 17px;
}
.feather-report p {
margin: 0;
color: #7c8795;
font-size: 11px;
}
.feather-report select,
.feather-report textarea {
width: 100%;
border: 0.5px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 10px;
padding: 10px;
color: inherit;
background: color-mix(in srgb, currentColor 5%, transparent);
font: inherit;
font-size: 12px;
}
.feather-report textarea {
min-height: 70px;
resize: none;
}
/* Feather's signed-in product surface follows the supplied X iOS references. */
.dark.feather-app {
--feather-blue: #1d9bf0;
--feather-border: #2f3336;
--feather-muted: #71767b;
display: flex;
flex-direction: column;
padding: 0 0 24px;
overflow: hidden;
background: #000;
color: #e7e9ea;
}
.dark.feather-app .feather-navbar {
--k-navbar-bg-color: color-mix(in srgb, #000 90%, transparent);
--k-safe-area-top: 46px;
border-bottom-color: var(--feather-border);
background: color-mix(in srgb, #000 88%, transparent);
backdrop-filter: blur(18px);
}
.dark.feather-app .feather-explore-search {
width: auto;
}
.dark.feather-app .feather-explore-search :deep(form) {
min-height: 36px;
border-radius: 999px;
color: #e7e9ea;
background: #202327;
}
.dark.feather-app .feather-main {
height: auto;
flex: 1;
padding-bottom: 58px;
background: #000;
}
.dark.feather-app .feather-scroll {
overscroll-behavior: contain;
scrollbar-width: none;
}
.dark.feather-app .feather-scroll::-webkit-scrollbar {
display: none;
}
.dark.feather-app .feather-feed-tabs,
.dark.feather-app .feather-explore-head,
.dark.feather-app .feather-activity-head,
.dark.feather-app .feather-profile-tabs {
border-bottom: 1px solid var(--feather-border);
background: #000;
}
.dark.feather-app .feather-context-tabs,
.dark.feather-app .feather-profile-tabs {
min-width: 100%;
padding: 0;
}
.dark.feather-app .feather-context-tabs :deep(.k-button),
.dark.feather-app .feather-profile-tabs :deep(.k-button) {
min-height: 46px;
border-radius: 0;
color: var(--feather-muted);
background: transparent;
font-size: 13px;
font-weight: 750;
white-space: nowrap;
}
.dark.feather-app .feather-context-tabs :deep(.k-button.is-active),
.dark.feather-app .feather-profile-tabs :deep(.k-button.is-active) {
color: #e7e9ea;
}
.dark.feather-app .feather-context-tabs :deep(.k-button.is-active)::after,
.dark.feather-app .feather-profile-tabs :deep(.k-button.is-active)::after {
right: 20%;
left: 20%;
height: 3px;
background: #eff3f4;
}
.dark.feather-app .feather-explore-tabs {
display: flex;
justify-content: flex-start !important;
overflow-x: auto !important;
overflow-y: hidden !important;
scrollbar-width: none;
}
.dark.feather-app .feather-explore-tabs :deep(.k-button) {
width: auto !important;
min-width: 92px;
flex: 0 0 auto !important;
}
.dark.feather-app .feather-explore-tabs::-webkit-scrollbar {
display: none;
}
.dark.feather-app .feather-trend {
position: relative;
min-height: 83px;
border-bottom: 1px solid var(--feather-border);
padding: 14px 42px 13px 14px;
color: #e7e9ea;
background: #000;
}
.dark.feather-app .feather-trend span,
.dark.feather-app .feather-trend small {
color: var(--feather-muted);
font-size: 11px;
font-weight: 550;
}
.dark.feather-app .feather-trend strong {
margin: 3px 0;
font-size: 15px;
line-height: 1.2;
}
.dark.feather-app .feather-trend b {
position: absolute;
top: 20px;
right: 14px;
color: var(--feather-muted);
font-size: 13px;
letter-spacing: 1px;
}
.dark.feather-app .feather-network__hero {
padding: 30px 22px 23px;
border-bottom: 1px solid var(--feather-border);
}
.dark.feather-app .feather-network__hero svg {
color: var(--feather-blue);
}
.dark.feather-app .feather-network__hero h1 {
margin: 13px 0 5px;
font-size: 25px;
letter-spacing: -0.8px;
}
.dark.feather-app .feather-network__hero p {
margin: 0;
color: var(--feather-muted);
font-size: 13px;
line-height: 1.45;
}
.dark.feather-app .feather-section-title {
border-bottom-color: var(--feather-border);
color: #e7e9ea;
background: #000;
font-size: 17px;
}
.dark.feather-app .feather-person,
.dark.feather-app .feather-activity {
align-items: flex-start;
border-bottom-color: var(--feather-border);
color: #e7e9ea;
background: #000;
}
.dark.feather-app .feather-person > button p {
display: -webkit-box;
margin: 5px 0 0;
overflow: hidden;
color: #e7e9ea;
font-size: 11px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.dark.feather-app .feather-person > :deep(.k-button) {
--k-button-bg-color: #eff3f4;
--k-button-text-color: #0f1419;
min-width: 66px;
font-weight: 800;
}
.dark.feather-app .feather-empty {
align-items: flex-start;
padding: 72px 28px;
color: var(--feather-muted);
text-align: left;
}
.dark.feather-app .feather-empty svg {
display: none;
}
.dark.feather-app .feather-empty h2 {
max-width: 245px;
margin: 0 0 10px;
color: #e7e9ea;
font-size: 28px;
line-height: 1.05;
letter-spacing: -1px;
}
.dark.feather-app .feather-empty p {
max-width: 250px;
font-size: 14px;
line-height: 1.4;
}
.dark.feather-app .feather-tab-icon b {
background: var(--feather-blue);
}
.dark.feather-app .feather-composer,
.dark.feather-app .feather-edit {
min-height: 100%;
color: #e7e9ea;
background: #000;
}
.dark.feather-app .feather-composer textarea {
min-height: 190px;
color: #e7e9ea;
caret-color: var(--feather-blue);
font-size: 18px;
}
.dark.feather-app .feather-composer__tools {
border-top-color: var(--feather-border);
}
.dark.feather-app .feather-profile-screen {
background: #000;
}
.dark.feather-app .feather-profile {
border-bottom-color: var(--feather-border);
background: #000;
}
.dark.feather-app .feather-profile__cover {
height: 154px;
background: linear-gradient(180deg, #168ad0, #1d9bf0);
}
.dark.feather-app .feather-profile__top {
height: 58px;
padding: 0 14px;
}
.dark.feather-app .feather-profile__avatar {
width: 84px;
height: 84px;
transform: translateY(-42px);
border: 4px solid #000;
background: #16181c;
}
.dark.feather-app .feather-profile h1 {
margin-top: 6px;
color: #e7e9ea;
font-size: 23px;
line-height: 1.15;
}
.dark.feather-app .feather-profile > span,
.dark.feather-app .feather-profile__joined,
.dark.feather-app .feather-profile__stats {
color: var(--feather-muted);
}
.dark.feather-app .feather-profile > span {
font-size: 14px;
}
.dark.feather-app .feather-profile > p {
margin-top: 13px;
color: #e7e9ea;
font-size: 14px;
}
.dark.feather-app .feather-profile__joined {
display: flex;
align-items: center;
gap: 5px;
margin: 10px 14px 0;
font-size: 12px;
}
.dark.feather-app .feather-profile__stats {
gap: 19px;
padding-top: 11px;
padding-bottom: 14px;
font-size: 13px;
}
.dark.feather-app .feather-profile__stats strong {
color: #e7e9ea;
}
.dark.feather-app .feather-profile__actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 0 14px 16px;
}
.dark.feather-app .feather-profile__actions :deep(.k-button) {
min-height: 39px;
border-color: #536471;
color: #e7e9ea;
font-size: 13px;
font-weight: 800;
}
.dark.feather-app .feather-profile-tabs :deep(.k-button) {
min-width: 62px;
gap: 5px;
}
.dark.feather-app .feather-profile-suggestions {
padding: 14px 0 17px;
border-bottom: 1px solid var(--feather-border);
}
.dark.feather-app .feather-profile-suggestions h2 {
margin: 0 14px 12px;
font-size: 20px;
}
.dark.feather-app .feather-profile-suggestions__rail {
display: flex;
gap: 10px;
overflow-x: auto;
padding: 0 14px;
scrollbar-width: none;
}
.dark.feather-app .feather-profile-suggestion {
position: relative;
width: 218px;
min-width: 218px;
overflow: hidden;
border: 1px solid var(--feather-border);
border-radius: 17px;
background: #000;
}
.dark.feather-app .feather-profile-suggestion__cover {
height: 76px;
background: linear-gradient(135deg, #123d64, #1d9bf0);
}
.dark.feather-app .feather-profile-suggestion > button {
display: flex;
width: 100%;
flex-direction: column;
border: 0;
padding: 31px 12px 12px;
color: #e7e9ea;
background: transparent;
text-align: left;
}
.dark.feather-app .feather-profile-suggestion__avatar {
position: absolute;
top: 46px;
left: 11px;
width: 58px;
height: 58px;
border: 3px solid #000;
}
.dark.feather-app .feather-profile-suggestion strong {
font-size: 16px;
}
.dark.feather-app .feather-profile-suggestion small {
color: var(--feather-muted);
font-size: 12px;
}
.dark.feather-app .feather-profile-suggestion p {
display: -webkit-box;
margin: 10px 0 0;
overflow: hidden;
font-size: 12px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.dark.feather-app .feather-profile-suggestion > :deep(.k-button) {
position: absolute;
right: 10px;
bottom: 74px;
z-index: 2;
--k-button-bg-color: #eff3f4;
--k-button-text-color: #0f1419;
min-width: 67px;
font-weight: 800;
}
.dark.feather-app .feather-settings-sheet,
.dark.feather-app .feather-action-sheet,
.dark.feather-app .feather-report {
color: #e7e9ea;
background: #000;
}
.feather-settings-sheet > header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 4px 10px;
}
.feather-settings-sheet > header span {
color: var(--feather-blue);
font-size: 10px;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
}
.feather-settings-sheet > header h2 {
margin: 1px 0 0;
font-size: 21px;
}
.dark.feather-app .feather-settings-sheet :deep(.k-list),
.dark.feather-app .feather-settings-sheet :deep(.k-list-item) {
color: #e7e9ea;
background: #16181c;
}
.feather-app--compact :deep(.feather-post) {
padding-block: 7px;
}
.feather-app--compact :deep(.feather-post__text) {
font-size: 12.5px;
}
/* Signed-in Feather mirrors the compact, card-led Local Pages shell. */
.feather-app--active {
--feather-blue: #58a6ff;
--feather-blue-dark: #2778dc;
--color-primary: var(--feather-blue);
--feather-panel: #20262c;
--feather-muted: #9ba4aa;
--feather-border: rgb(255 255 255 / 8%);
position: relative;
height: 100%;
padding: 0;
overflow: hidden;
background: #12171b !important;
color: #f7f8f4;
font-family: Inter, system-ui, sans-serif;
}
.feather-app--active.feather-app--light {
--feather-panel: #f0f1ec;
--feather-muted: #70797e;
--feather-border: rgb(0 0 0 / 8%);
background: #fbfbf6 !important;
color: #171b1e;
}
.feather-app--active button,
.feather-app--active input,
.feather-app--active textarea,
.feather-app--active select {
font: inherit;
}
.feather-app--active .feather-navbar {
--k-navbar-bg-color: color-mix(in srgb, #12171b 91%, transparent);
--k-safe-area-top: 46px;
position: absolute;
z-index: 8;
top: 0;
right: 0;
left: 0;
flex: none;
border-bottom: 0;
background: color-mix(in srgb, #12171b 88%, transparent);
backdrop-filter: blur(18px);
}
.feather-app--active.feather-app--light .feather-navbar {
--k-navbar-bg-color: color-mix(in srgb, #fbfbf6 91%, transparent);
background: color-mix(in srgb, #fbfbf6 88%, transparent);
}
.feather-app--active.feather-app--section .feather-navbar {
border-bottom: 1px solid var(--feather-border);
}
.feather-navbar__brand-mark {
display: block;
color: currentColor;
filter: drop-shadow(0 3px 8px rgb(0 0 0 / 18%));
transform: translateY(4px);
}
.feather-app--active .feather-main {
position: absolute;
inset: 0;
display: flex;
height: auto;
min-height: 0;
flex-direction: column;
gap: 9px;
padding: 108px 13px 105px;
background: transparent;
}
.feather-app--active.feather-app--home .feather-main {
padding-top: 106px;
}
.feather-app--active .feather-scroll {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
gap: 10px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: none;
}
.feather-app--active .feather-scroll::-webkit-scrollbar {
display: none;
}
.feather-hero-glass,
.feather-network-glass,
.feather-profile-glass {
display: block;
width: 100%;
flex: none;
overflow: hidden;
border-radius: 17px;
}
.feather-hero {
display: flex;
min-height: 105px;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 15px;
color: #e9f5ff;
background: linear-gradient(125deg, #173f6d, #2778dc);
}
.feather-hero > div {
min-width: 0;
max-width: 215px;
}
.feather-hero small,
.feather-hero strong,
.feather-hero span {
display: block;
}
.feather-hero small,
.feather-network__hero small {
color: #a8d4ff;
font-size: 9.5px;
font-weight: 900;
line-height: 1.1;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.feather-hero strong {
margin: 4px 0 3px;
font-size: 18px;
line-height: 1.12;
}
.feather-hero span {
font-size: 11px;
line-height: 1.3;
}
.feather-hero > svg {
flex: none;
color: #b9ddff;
filter: drop-shadow(0 5px 8px rgb(0 0 0 / 24%));
}
.feather-app--active .feather-feed-tabs,
.feather-app--active .feather-activity-head,
.feather-app--active .feather-profile-tabs {
flex: none;
overflow: hidden;
border: 0;
border-radius: 13px;
padding: 4px;
background: var(--feather-panel);
}
.feather-app--active .feather-feed-tabs {
display: flex;
align-items: stretch;
gap: 10px;
}
.feather-app--active .feather-feed-tabs .feather-context-tabs {
min-width: 0;
flex: 1;
}
.feather-app--active .feather-feed-add {
--k-button-bg-color: var(--feather-blue-dark);
--k-button-text-color: #fff;
width: 76px !important;
min-width: 76px;
min-height: 34px;
flex: 0 0 76px !important;
gap: 5px;
border-radius: 9px;
padding: 0 11px;
box-shadow: 0 4px 12px rgb(20 80 155 / 24%);
font-size: 11px;
font-weight: 850;
transition:
transform 160ms ease,
box-shadow 160ms ease,
filter 160ms ease;
}
.feather-app--active .feather-explore-head {
display: grid;
flex: none;
gap: 7px;
overflow: hidden;
border: 0;
border-radius: 17px;
padding: 9px;
background: var(--feather-panel);
}
.feather-app--active .feather-explore-search {
width: auto;
margin: 0;
}
.feather-app--active .feather-explore-search :deep(form) {
min-height: 39px;
border-radius: 12px;
color: inherit;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.feather-app--active .feather-context-tabs,
.feather-app--active .feather-profile-tabs {
min-width: 100%;
padding: 0;
background: transparent;
}
.feather-app--active .feather-context-tabs :deep(.k-button),
.feather-app--active .feather-profile-tabs :deep(.k-button) {
min-height: 34px;
overflow: hidden;
border-radius: 9px;
color: var(--feather-muted);
background: transparent;
font-size: 11.5px;
font-weight: 750;
white-space: nowrap;
}
.feather-app--active .feather-context-tabs :deep(.k-button.is-active),
.feather-app--active .feather-profile-tabs :deep(.k-button.is-active) {
color: #fff;
background: var(--feather-blue-dark);
}
.feather-app--active.feather-app--light
.feather-context-tabs
:deep(.k-button.is-active),
.feather-app--active.feather-app--light
.feather-profile-tabs
:deep(.k-button.is-active) {
color: #fff;
}
.feather-app--active .feather-context-tabs :deep(.k-button.is-active)::after,
.feather-app--active .feather-profile-tabs :deep(.k-button.is-active)::after {
display: none;
}
.feather-app--active .feather-feed-tabs {
overflow: visible;
border-bottom: 1px solid var(--feather-border);
border-radius: 0;
margin-right: -13px;
margin-left: -13px;
padding: 0 13px;
background: transparent;
}
.feather-app--active .feather-feed-tabs .feather-context-tabs {
overflow: visible;
background: transparent;
}
.feather-app--active .feather-feed-tabs .feather-context-tabs :deep(.k-button) {
position: relative;
min-height: 42px;
overflow: visible;
border-radius: 0;
color: var(--feather-muted);
background: transparent !important;
box-shadow: none;
}
.feather-app--active
.feather-feed-tabs
.feather-context-tabs
:deep(.k-button.is-active) {
color: inherit;
background: transparent !important;
}
.feather-app--active
.feather-feed-tabs
.feather-context-tabs
:deep(.k-button.is-active)::after {
position: absolute;
right: 24%;
bottom: -1px;
left: 24%;
display: block;
height: 3px;
border-radius: 999px 999px 0 0;
background: var(--feather-blue);
content: '';
}
.feather-app--active .feather-feed-add {
margin-block: 4px;
}
@media (hover: hover) {
.feather-app--active .feather-feed-add:hover {
filter: brightness(1.13);
box-shadow: 0 7px 17px rgb(39 120 220 / 38%);
transform: translateY(-2px);
}
}
.feather-app--active .feather-feed-add:active {
filter: brightness(0.96);
box-shadow: 0 3px 9px rgb(20 80 155 / 26%);
transform: translateY(0) scale(0.97);
}
.feather-app--active .feather-explore-tabs {
display: flex;
width: 100%;
justify-content: stretch !important;
gap: 4px;
overflow: hidden !important;
}
.feather-app--active .feather-explore-tabs :deep(.k-button) {
width: 0 !important;
min-width: 0;
flex: 1 1 0 !important;
}
.feather-app--active .feather-main > .feather-scroll {
padding-top: 1px;
}
.feather-app--active :deep(.feather-post-glass) {
flex: none;
border: 1px solid var(--feather-border);
background: color-mix(in srgb, var(--feather-panel) 90%, transparent);
box-shadow: 0 5px 18px rgb(0 0 0 / 10%);
}
.feather-app--active :deep(.feather-post) {
padding: 11px 11px 8px;
}
.feather-app--active :deep(.feather-post__header strong) {
font-size: 13px;
}
.feather-app--active :deep(.feather-post__header span) {
color: var(--feather-muted);
font-size: 10.5px;
}
.feather-app--active :deep(.feather-post__text) {
font-size: 12.5px;
line-height: 1.42;
}
.feather-app--active :deep(.feather-actions) {
color: var(--feather-muted);
}
.feather-app--active :deep(.feather-follow) {
--k-button-bg-color: transparent;
--k-button-text-color: var(--feather-blue);
}
.feather-app--active .feather-trend,
.feather-app--active .feather-person,
.feather-app--active .feather-activity {
position: relative;
width: 100%;
flex: none;
border: 1px solid var(--feather-border);
border-radius: 15px;
padding: 11px;
color: inherit;
background: var(--feather-panel);
box-shadow: 0 5px 18px rgb(0 0 0 / 9%);
}
.feather-app--active .feather-trend {
min-height: 76px;
padding-right: 40px;
}
.feather-app--active .feather-trend span,
.feather-app--active .feather-trend small,
.feather-app--active .feather-person span {
color: var(--feather-muted);
font-size: 10.5px;
}
.feather-app--active .feather-trend strong {
margin: 3px 0;
font-size: 14px;
}
.feather-app--active .feather-trend b {
position: absolute;
top: 15px;
right: 12px;
color: var(--feather-muted);
font-size: 11px;
letter-spacing: 1px;
}
.feather-network-glass {
margin-bottom: 2px;
}
.feather-app--active .feather-network-search {
width: auto;
flex: none;
margin: 0;
}
.feather-app--active .feather-network-search :deep(form) {
min-height: 40px;
border: 1px solid var(--feather-border);
border-radius: 13px;
color: inherit;
background: var(--feather-panel);
box-shadow: 0 5px 18px rgb(0 0 0 / 9%);
}
.feather-network-loading {
display: grid;
min-height: 80px;
flex: none;
place-items: center;
}
.feather-network-section {
display: flex;
flex: none;
flex-direction: column;
gap: 8px;
}
.feather-network-list {
overflow: hidden;
margin: 0 !important;
border: 1px solid var(--feather-border);
border-radius: 18px;
background: color-mix(in srgb, var(--feather-panel) 97%, transparent);
box-shadow: 0 7px 22px rgb(0 0 0 / 9%);
}
.feather-network-person :deep(.feather-network-person__content) {
min-height: 91px;
align-items: flex-start;
padding-block: 11px;
}
.feather-network-person :deep(.feather-network-person__media) {
margin-right: 10px;
padding-block: 0;
}
.feather-network-person :deep(.feather-network-person__inner) {
min-width: 0;
padding-block: 0 7px;
}
.feather-network-person :deep(.feather-network-person__title-wrap) {
align-items: center;
}
.feather-network-person
:deep(.feather-network-person__title-wrap > div:first-child) {
min-width: 0;
flex: 1;
}
.feather-network-person__handle {
display: block;
margin-top: 1px;
color: var(--feather-muted);
font-size: 9.5px;
line-height: 1.2;
}
.feather-network-person__bio {
display: -webkit-box;
overflow: hidden;
margin-top: 5px;
color: color-mix(in srgb, currentColor 82%, var(--feather-muted));
font-size: 10px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feather-network-person__avatar {
display: block;
border: 0;
border-radius: 50%;
padding: 0;
background: transparent;
}
.feather-network-person__avatar .feather-avatar {
width: 44px;
height: 44px;
border: 2px solid color-mix(in srgb, var(--feather-blue) 25%, transparent);
box-shadow: 0 5px 13px rgb(0 0 0 / 14%);
}
.feather-network-person__name {
display: flex;
width: 100%;
min-width: 0;
max-width: 100%;
align-items: center;
gap: 4px;
overflow: hidden;
border: 0;
padding: 0;
color: inherit;
background: transparent;
font: inherit;
font-size: 11.5px;
font-weight: 850;
text-align: left;
}
.feather-network-person__name span {
display: block;
min-width: 0;
max-width: 100%;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-network-person__name svg {
flex: none;
color: var(--feather-blue);
}
.feather-network-person__stats {
display: flex;
align-items: center;
gap: 5px;
color: var(--feather-muted);
font-size: 8.5px;
font-weight: 600;
}
.feather-network-person__stats strong {
color: inherit;
font-size: inherit;
}
.feather-network-person__stats i {
width: 2px;
height: 2px;
border-radius: 50%;
background: currentColor;
}
.feather-network-person__follow {
min-width: 63px !important;
min-height: 28px !important;
padding-inline: 8px !important;
font-size: 9px !important;
}
.feather-app--active .feather-network-empty {
display: flex;
min-height: 112px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 4px;
margin: 0;
border: 1px solid var(--feather-border);
color: var(--feather-muted);
background: var(--feather-panel);
text-align: center;
}
.feather-app--active .feather-network-empty strong {
color: inherit;
font-size: 13px;
}
.feather-app--active .feather-network-empty p {
margin: 0;
font-size: 10.5px;
}
.feather-app--active .feather-network__hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 0;
padding: 15px;
background: linear-gradient(125deg, #173f6d, #2778dc);
}
.feather-app--active .feather-network__hero > div {
min-width: 0;
}
.feather-app--active .feather-network__hero h1 {
margin: 4px 0 3px;
color: #fff;
font-size: 19px;
letter-spacing: -0.4px;
}
.feather-app--active .feather-network__hero p {
margin: 0;
color: #d8ebff;
font-size: 10.5px;
line-height: 1.35;
}
.feather-app--active .feather-network__hero > svg {
flex: none;
color: #b9ddff;
}
.feather-app--active .feather-section-title {
flex: none;
border: 0;
padding: 7px 3px 1px;
color: inherit;
background: transparent;
font-size: 14px;
}
.feather-app--active .feather-person > button p {
display: -webkit-box;
margin: 4px 0 0;
overflow: hidden;
color: var(--feather-muted);
font-size: 10.5px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feather-app--active .feather-person > :deep(.k-button) {
min-width: 72px;
font-size: 10.5px;
font-weight: 800;
}
.feather-app--active .feather-follow-button {
min-width: 72px;
min-height: 31px;
border-width: 1px;
padding-inline: 10px 12px;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.1px;
transition:
transform 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
.feather-app--active .feather-follow-button :deep(.k-icon) {
margin-right: 4px;
}
.feather-app--active .feather-follow-button--pending {
--k-button-bg-color: var(--feather-blue) !important;
--k-button-text-color: #fff !important;
border-color: color-mix(in srgb, var(--feather-blue) 76%, #fff);
box-shadow: 0 4px 12px rgb(29 155 240 / 24%);
}
.feather-app--active .feather-follow-button--following {
--k-button-bg-color: transparent !important;
--k-button-text-color: currentColor !important;
border-color: color-mix(in srgb, currentColor 22%, transparent);
box-shadow: none;
}
@media (hover: hover) {
.feather-app--active .feather-follow-button--pending:hover,
.feather-app--active :deep(.feather-follow:hover) {
transform: translateY(-1px);
background: color-mix(in srgb, var(--feather-blue) 10%, transparent);
}
}
.feather-app--active .feather-empty {
min-height: 230px;
justify-content: center;
padding: 35px 25px;
color: var(--feather-muted);
text-align: center;
}
.feather-app--active .feather-empty h2 {
margin: 10px 0 4px;
color: inherit;
font-size: 16px;
letter-spacing: -0.2px;
}
.feather-app--active .feather-thread-reply {
margin: 10px 0 5px;
border: 1px solid var(--feather-border);
border-radius: 16px;
background: color-mix(in srgb, var(--feather-panel) 88%, transparent);
}
.feather-thread-reply form {
display: flex;
align-items: flex-start;
gap: 9px;
padding: 11px;
}
.feather-thread-reply form > div {
min-width: 0;
flex: 1;
}
.feather-thread-reply__avatar {
width: 34px;
height: 34px;
flex: 0 0 34px;
}
.feather-thread-reply__target {
display: block;
margin-bottom: 3px;
color: var(--feather-blue);
font-size: 9.5px;
font-weight: 700;
}
.feather-thread-reply__comment-target {
display: flex;
align-items: flex-start;
gap: 7px;
margin-bottom: 7px;
border-left: 2px solid var(--feather-blue);
border-radius: 0 10px 10px 0;
padding: 7px 6px 7px 9px;
background: color-mix(in srgb, var(--feather-blue) 9%, transparent);
}
.feather-thread-reply__comment-target > div {
min-width: 0;
flex: 1;
}
.feather-thread-reply__comment-target .feather-thread-reply__target {
margin-bottom: 2px;
}
.feather-thread-reply__comment-target p {
display: -webkit-box;
overflow: hidden;
margin: 0;
color: var(--feather-muted);
font-size: 9.5px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feather-thread-reply__comment-target :deep(.k-button) {
width: 25px;
min-width: 25px;
height: 25px;
min-height: 25px;
padding: 0;
color: var(--feather-muted);
}
.feather-thread-reply textarea {
display: block;
width: 100%;
min-height: 58px;
border: 0;
outline: 0;
padding: 3px 0 7px;
color: inherit;
background: transparent;
caret-color: var(--feather-blue);
font: inherit;
font-size: 13px;
line-height: 1.4;
resize: none;
}
.feather-thread-reply textarea::placeholder {
color: var(--feather-muted);
}
.feather-thread-reply footer {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--feather-border);
padding-top: 7px;
}
.feather-thread-reply footer > span {
color: var(--feather-muted);
font-size: 9px;
}
.feather-thread-reply footer :deep(.k-button) {
min-width: 62px;
min-height: 29px;
font-size: 10.5px;
font-weight: 800;
}
.feather-app--active .feather-thread-title {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 10px;
}
.feather-thread-title span {
display: grid;
min-width: 20px;
height: 20px;
place-items: center;
border-radius: 999px;
color: var(--feather-blue);
background: color-mix(in srgb, var(--feather-blue) 14%, transparent);
font-size: 9.5px;
}
.feather-thread-empty {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 22px 12px;
color: var(--feather-muted);
font-size: 11px;
}
.feather-thread-empty svg {
color: var(--feather-blue);
}
.feather-app--active .feather-empty p {
max-width: 230px;
font-size: 12px;
}
.feather-tab-pane {
width: 100% !important;
max-width: none;
margin-right: auto;
margin-left: auto;
flex: none;
gap: 2px;
padding: 0 4px;
}
:global(.feather-tab-pane) {
width: 100% !important;
max-width: none;
margin-right: auto;
margin-left: auto;
flex: none;
}
:global(.feather-tab-button) {
width: auto !important;
min-width: 0 !important;
max-width: none !important;
flex: 1 1 0 !important;
padding-right: 3px !important;
padding-left: 3px !important;
}
.feather-tab-label {
display: block;
max-width: 52px;
overflow: hidden;
font-size: 9.5px;
line-height: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-tab-icon {
position: relative;
display: grid;
place-items: center;
}
.feather-compose-fab {
position: absolute;
z-index: 12;
right: 14px;
bottom: 91px;
width: 46px;
height: 46px;
min-width: 46px;
border: 1px solid color-mix(in srgb, var(--feather-blue) 55%, #fff);
color: #fff;
box-shadow:
0 9px 24px rgb(29 155 240 / 38%),
0 3px 8px rgb(0 0 0 / 22%),
inset 0 1px 0 rgb(255 255 255 / 32%);
transition:
transform 150ms ease,
box-shadow 150ms ease,
filter 150ms ease;
}
.feather-compose-fab:active {
filter: brightness(0.94);
transform: scale(0.94);
box-shadow:
0 4px 12px rgb(29 155 240 / 28%),
inset 0 1px 0 rgb(255 255 255 / 22%);
}
.feather-tab-icon b {
position: absolute;
top: -5px;
right: -8px;
min-width: 14px;
border-radius: 8px;
padding: 1px 3px;
color: #fff;
background: #f04f65;
font-size: 8px;
text-align: center;
}
.feather-app--active > .feather-scroll,
.feather-app--active > .feather-composer,
.feather-app--active > .feather-edit {
position: absolute;
top: 104px;
right: 0;
bottom: 24px;
left: 0;
min-height: 0;
overflow-y: auto;
}
.feather-app--active > .feather-scroll {
padding: 13px;
}
.feather-app--active > .feather-profile-screen {
bottom: 96px;
}
.feather-profile-glass {
flex: none;
}
.feather-app--active .feather-profile-glass {
overflow: hidden;
border: 1px solid var(--feather-border);
border-radius: 18px;
}
.feather-app--active .feather-profile {
overflow: hidden;
border: 0;
background: transparent;
}
.feather-app--active .feather-profile__cover {
height: 112px;
background: linear-gradient(135deg, #173f6d, #58a6ff);
}
.feather-app--active .feather-profile__top {
height: 50px;
padding: 0 13px;
}
.feather-app--active .feather-profile__avatar {
width: 76px;
height: 76px;
transform: translateY(-38px);
border: 3px solid var(--feather-panel);
background: linear-gradient(145deg, #72c9ff, #377be7);
}
.feather-app--active .feather-profile__identity {
padding: 4px 13px 0;
}
.feather-app--active .feather-profile__identity h1 {
color: inherit;
font-size: 20px;
}
.feather-app--active .feather-profile__handle,
.feather-app--active .feather-profile__joined,
.feather-app--active .feather-profile__stats {
color: var(--feather-muted);
}
.feather-app--active .feather-profile__handle {
font-size: 11.5px;
}
.feather-app--active .feather-profile__bio {
margin: 10px 0 8px;
color: inherit;
font-size: 12.5px;
line-height: 1.45;
}
.feather-app--active .feather-profile__joined {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 5px;
margin: 0;
font-size: 11px;
line-height: 1;
white-space: nowrap;
}
.feather-app--active .feather-profile__joined svg {
flex: none;
}
.feather-app--active .feather-profile__stats {
padding: 12px 13px;
font-size: 11.5px;
}
.feather-app--active .feather-profile__stats span {
border: 1px solid var(--feather-border);
background: color-mix(in srgb, var(--feather-blue) 8%, transparent);
}
.feather-app--active .feather-profile__stats strong {
color: var(--feather-blue);
}
.feather-app--active .feather-profile__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
padding: 0 13px 13px;
}
.feather-app--active .feather-profile__actions :deep(.k-button) {
width: 100%;
min-width: 0;
min-height: 36px;
gap: 6px;
border-color: color-mix(in srgb, var(--feather-blue) 55%, transparent);
color: inherit;
padding-inline: 7px;
font-size: 10.5px;
font-weight: 750;
}
.feather-app--active .feather-profile-action span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-app--active .feather-profile-action svg {
flex: none;
}
.feather-app--active .feather-profile-tabs {
margin-top: 1px;
}
.feather-app--active .feather-profile-suggestions {
flex: none;
margin-top: 7px;
border: 0;
border-radius: 17px;
padding: 13px 0;
background: var(--feather-panel);
}
.feather-app--active .feather-profile-suggestions h2 {
margin: 0 13px 10px;
font-size: 16px;
}
.feather-app.feather-app--active .feather-profile-suggestions__rail {
display: flex;
gap: 9px;
overflow-x: auto;
padding: 0 13px;
scrollbar-width: none;
}
.feather-app.feather-app--active .feather-profile-suggestion {
position: relative;
width: 240px;
min-width: 240px;
min-height: 88px;
overflow: hidden;
border: 1px solid var(--feather-border);
border-radius: 15px;
border-color: var(--feather-border);
background: color-mix(in srgb, var(--feather-panel) 87%, #111);
}
.feather-app--active.feather-app--light .feather-profile-suggestion {
background: #fff;
}
.feather-app.feather-app--active .feather-profile-suggestion > button {
display: grid;
width: 100%;
grid-template-columns: 42px minmax(0, 1fr);
align-items: start;
gap: 9px;
border: 0;
padding: 11px 72px 11px 11px;
color: inherit;
background: transparent;
text-align: left;
}
.feather-app.feather-app--active .feather-profile-suggestion__avatar {
position: static;
width: 42px;
height: 42px;
border: 0;
}
.feather-app--active .feather-profile-suggestion__identity {
min-width: 0;
}
.feather-app--active .feather-profile-suggestion__identity strong,
.feather-app--active .feather-profile-suggestion__identity small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-app--active .feather-profile-suggestion__identity strong {
font-size: 12.5px;
}
.feather-app--active .feather-profile-suggestion__identity small {
color: var(--feather-muted);
font-size: 10.5px;
}
.feather-app--active .feather-profile-suggestion__identity p {
display: -webkit-box;
margin: 5px 0 0;
overflow: hidden;
font-size: 10.5px;
line-height: 1.3;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.feather-app.feather-app--active
.feather-profile-suggestion
> :deep(.k-button) {
position: absolute;
top: 10px;
right: 9px;
bottom: auto;
min-width: 56px;
min-height: 27px;
padding-inline: 8px;
font-size: 10px;
}
.feather-app--active .feather-composer,
.feather-app--active .feather-edit {
color: inherit;
background: transparent;
}
.feather-app--active .feather-composer {
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
border: 0;
border-radius: 0;
padding: 13px 13px 34px;
background: transparent;
}
.feather-composer-close {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid var(--feather-border);
border-radius: 50%;
color: inherit;
background: var(--feather-panel);
}
.feather-composer-publish,
.feather-edit__navbar-save {
min-width: 58px;
min-height: 30px;
padding-inline: 12px;
font-size: 10px;
font-weight: 800;
}
.feather-composer-card {
flex: none;
overflow: hidden;
border-radius: 18px;
}
.feather-composer-card__inner {
padding: 13px;
}
.feather-composer__identity {
display: flex;
align-items: center;
gap: 9px;
}
.feather-composer__identity > div:last-child {
min-width: 0;
}
.feather-composer__identity strong,
.feather-composer__identity span {
display: block;
}
.feather-composer__identity strong {
overflow: hidden;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-composer__identity span {
color: var(--feather-muted);
font-size: 10.5px;
}
.feather-app--active .feather-composer-card textarea {
min-height: 135px;
margin-top: 9px;
padding: 4px 1px 10px;
color: inherit;
caret-color: var(--feather-blue);
font-size: 15px;
line-height: 1.45;
}
.feather-composer-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-top: 1px solid var(--feather-border);
padding-top: 9px;
color: var(--feather-muted);
font-size: 10px;
}
.feather-composer-card__footer span {
display: flex;
min-width: 0;
align-items: center;
gap: 5px;
color: var(--feather-blue);
}
.feather-composer-card__footer b {
flex: none;
font-weight: 750;
}
.feather-composer-media {
flex: none;
border: 1px solid var(--feather-border);
border-radius: 18px;
padding: 13px;
background: var(--feather-panel);
}
.feather-composer-media > header {
display: flex;
align-items: center;
gap: 9px;
}
.feather-composer-media > header > span {
display: grid;
width: 36px;
height: 36px;
flex: none;
place-items: center;
border-radius: 11px;
color: var(--feather-blue);
background: color-mix(in srgb, var(--feather-blue) 14%, transparent);
}
.feather-composer-media > header > div {
min-width: 0;
flex: 1;
}
.feather-composer-media > header strong,
.feather-composer-media > header small {
display: block;
}
.feather-composer-media > header strong {
font-size: 13px;
}
.feather-composer-media > header small {
margin-top: 1px;
color: var(--feather-muted);
font-size: 9.5px;
line-height: 1.3;
}
.feather-composer-media > header > b {
flex: none;
border-radius: 8px;
padding: 4px 7px;
color: var(--feather-blue);
background: color-mix(in srgb, var(--feather-blue) 11%, transparent);
font-size: 10px;
}
.feather-composer-media__actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 11px;
}
.feather-composer-media__actions > * {
min-width: 0;
overflow: hidden;
border-radius: 14px;
}
.feather-composer-media__actions button {
width: 100%;
min-height: 105px;
border: 0;
padding: 10px;
color: inherit;
background: transparent;
text-align: left;
}
.feather-composer-media__actions button > span {
display: grid;
width: 32px;
height: 32px;
margin-bottom: 7px;
place-items: center;
border-radius: 10px;
color: #fff;
background: var(--feather-blue-dark);
}
.feather-composer-media__actions button strong,
.feather-composer-media__actions button small {
display: block;
}
.feather-composer-media__actions button strong {
font-size: 11.5px;
}
.feather-composer-media__actions button small {
margin-top: 2px;
color: var(--feather-muted);
font-size: 9px;
line-height: 1.25;
}
.feather-composer-media__actions button:disabled {
opacity: 0.38;
}
@media (hover: hover) {
.feather-composer-media__actions button:not(:disabled):hover {
background: color-mix(in srgb, var(--feather-blue) 9%, transparent);
}
}
.feather-composer-media__selected {
margin-top: 13px;
}
.feather-composer-media__selected-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 7px;
font-size: 11px;
}
.feather-composer-media__selected-head span {
color: var(--feather-blue);
font-size: 10px;
font-weight: 800;
}
.feather-app--active .feather-compose-media {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin: 0;
}
.feather-app--active .feather-compose-media--1 {
grid-template-columns: 1fr;
}
.feather-app--active .feather-compose-media > div {
min-height: 94px;
border: 1px solid var(--feather-border);
border-radius: 12px;
}
.feather-app--active .feather-compose-media--1 > div {
min-height: 165px;
}
.feather-app--active .feather-compose-media img {
height: 100%;
min-height: 94px;
}
.feather-app--active .feather-compose-media i {
position: absolute;
bottom: 5px;
left: 5px;
display: grid;
width: 18px;
height: 18px;
place-items: center;
border-radius: 50%;
color: #fff;
background: rgb(0 0 0 / 68%);
font-size: 8px;
font-style: normal;
font-weight: 850;
}
.feather-app--active .feather-edit {
display: flex;
flex-direction: column;
gap: 13px;
padding: 14px 13px 34px;
}
.feather-navbar :deep(.feather-navbar__plain-action) {
background-image: none !important;
background: transparent !important;
box-shadow: none !important;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.feather-navbar :deep(.feather-navbar__plain-action > span) {
display: none !important;
}
.feather-edit__identity {
position: relative;
display: flex;
align-items: center;
gap: 12px;
overflow: hidden;
border: 1px solid var(--feather-border);
border-radius: 18px;
padding: 13px;
background:
radial-gradient(
circle at 100% 0,
color-mix(in srgb, var(--feather-blue) 22%, transparent),
transparent 48%
),
color-mix(in srgb, var(--feather-panel) 94%, transparent);
box-shadow: 0 10px 28px rgb(0 0 0 / 8%);
}
.feather-edit__avatar {
display: grid;
width: 58px;
height: 58px;
flex: 0 0 58px;
place-items: center;
overflow: hidden;
border: 2px solid color-mix(in srgb, var(--feather-blue) 58%, #fff);
border-radius: 50%;
color: #fff;
background: linear-gradient(145deg, #71c8ff, #377be7);
box-shadow: 0 7px 18px rgb(29 155 240 / 22%);
}
.feather-edit__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.feather-edit__account {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.feather-edit__account strong {
overflow: hidden;
font-size: 15px;
line-height: 1.25;
letter-spacing: -0.2px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-edit__account span {
overflow: hidden;
margin-top: 3px;
color: var(--feather-muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-edit__badge {
display: grid;
width: 30px;
height: 30px;
flex: 0 0 30px;
place-items: center;
border-radius: 50%;
color: #fff;
background: var(--feather-blue);
box-shadow: 0 5px 14px rgb(29 155 240 / 25%);
}
.feather-edit__photo {
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid var(--feather-border);
border-radius: 18px;
padding: 11px 12px 12px;
background: color-mix(in srgb, var(--feather-panel) 96%, transparent);
box-shadow: 0 10px 28px rgb(0 0 0 / 7%);
}
.feather-edit__photo > strong {
font-size: 11px;
font-weight: 800;
}
.feather-edit__photo-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.feather-edit__photo-actions :deep(.k-button) {
min-width: 0;
min-height: 35px;
gap: 6px;
border: 1px solid color-mix(in srgb, var(--feather-blue) 24%, transparent);
color: var(--feather-blue);
font-size: 10px;
font-weight: 800;
}
.feather-edit__fields {
overflow: hidden;
margin: 0 !important;
border: 1px solid var(--feather-border);
border-radius: 18px;
background: color-mix(in srgb, var(--feather-panel) 96%, transparent);
box-shadow: 0 10px 28px rgb(0 0 0 / 7%);
}
.feather-edit__fields :deep(.k-list-item-media) {
align-self: flex-start;
margin-top: 20px;
color: var(--feather-blue);
}
.feather-edit__fields :deep(input),
.feather-edit__fields :deep(textarea) {
font-size: 13px;
line-height: 1.45;
}
.feather-edit__fields :deep(textarea) {
min-height: 128px;
resize: none;
}
.feather-edit__fields :deep(.k-list-input-info) {
color: var(--feather-muted);
font-size: 9px;
}
.feather-connections {
padding-top: 10px !important;
}
.feather-connections__tabs {
width: 100%;
margin-bottom: 11px;
border: 1px solid var(--feather-border);
border-radius: 15px;
padding: 3px;
background: color-mix(in srgb, var(--feather-panel) 94%, transparent);
}
.feather-connections__tabs :deep(button) {
min-height: 31px;
border-radius: 11px !important;
font-size: 10px;
font-weight: 800;
}
.feather-connections__loading {
display: grid;
min-height: 180px;
place-items: center;
color: var(--feather-blue);
}
.feather-connections__empty {
display: flex;
min-height: 180px;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 !important;
border: 1px solid var(--feather-border);
border-radius: 18px;
color: var(--feather-muted);
text-align: center;
}
.feather-connections__empty strong {
margin-top: 9px;
color: inherit;
font-size: 14px;
}
.feather-connections__empty p {
max-width: 210px;
margin: 4px 0 0;
font-size: 10px;
line-height: 1.4;
}
.feather-connections__list {
overflow: hidden;
margin: 0 !important;
border: 1px solid var(--feather-border);
border-radius: 18px;
background: color-mix(in srgb, var(--feather-panel) 96%, transparent);
}
.feather-connections__list :deep(.k-list-item-content) {
min-height: 62px;
}
.feather-connections__list :deep(.k-list-item-title) {
max-width: 108px;
overflow: hidden;
font-size: 12px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-connections__list :deep(.k-list-item-text) {
max-width: 108px;
overflow: hidden;
color: var(--feather-muted);
font-size: 9px;
text-overflow: ellipsis;
white-space: nowrap;
}
.feather-connections__profile {
border: 0;
padding: 0;
background: transparent;
}
.feather-connections__avatar {
width: 39px;
height: 39px;
}
.feather-connections__remove {
min-width: 70px;
min-height: 29px;
gap: 4px;
border-color: color-mix(in srgb, #f04f65 62%, transparent);
color: #f04f65;
font-size: 9px;
font-weight: 800;
}
.feather-app--active .feather-settings-sheet,
.feather-app--active .feather-action-sheet,
.feather-app--active .feather-report {
color: inherit;
background: var(--feather-panel);
}
</style>