first commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { NativeLink } from '@native-vue-router/core'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { relativeTime, useDemoStore } from '../data'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useDemoStore()
|
||||
const id = computed(() => String(route.params.id))
|
||||
const conversation = computed(() => store.conversations.value.find((item) => item.id === id.value))
|
||||
const person = computed(() => store.personFor(id.value))
|
||||
const draft = ref('')
|
||||
const list = ref<HTMLElement | null>(null)
|
||||
|
||||
async function send() {
|
||||
const body = draft.value
|
||||
if (!body.trim()) return
|
||||
draft.value = ''
|
||||
void store.sendMessage(id.value, body)
|
||||
await nextTick()
|
||||
list.value?.scrollTo({ top: list.value.scrollHeight, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.markRead(id.value)
|
||||
list.value?.scrollTo({ top: list.value.scrollHeight })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="conversation && person" class="screen chat-screen">
|
||||
<AppHeader :title="person.name" :subtitle="person.online ? 'Online now' : person.handle" back>
|
||||
<NativeLink :to="`/chat/${id}/details`" class="avatar-button" aria-label="Conversation details">
|
||||
<AppAvatar :person="person" size="sm" />
|
||||
</NativeLink>
|
||||
</AppHeader>
|
||||
|
||||
<div ref="list" class="message-list">
|
||||
<div class="message-day">Today</div>
|
||||
<article v-for="message in conversation.messages" :key="message.id" class="message" :class="{ 'message--mine': message.mine }">
|
||||
<p>{{ message.body }}</p>
|
||||
<footer>
|
||||
<time>{{ relativeTime(message.sentAt) }}</time>
|
||||
<span v-if="message.mine">{{ message.status === 'failed' ? 'Tap to retry' : message.status }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
<div v-if="person.online" class="typing-pill"><i /><i /><i /></div>
|
||||
</div>
|
||||
|
||||
<form class="composer" @submit.prevent="send">
|
||||
<button type="button" aria-label="Add attachment">+</button>
|
||||
<input v-model="draft" aria-label="Message" :placeholder="`Message ${person.name.split(' ')[0]}`" />
|
||||
<button class="send-button" type="submit" :disabled="!draft.trim()" aria-label="Send">↑</button>
|
||||
</form>
|
||||
</main>
|
||||
<main v-else class="screen empty-state"><h1>Conversation not found</h1></main>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { NativeDismissGesture, useNativeRouter } from '@native-vue-router/core'
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import { useDemoStore } from '../data'
|
||||
|
||||
const native = useNativeRouter()
|
||||
const store = useDemoStore()
|
||||
const query = ref('')
|
||||
|
||||
async function choose(id: string) {
|
||||
await native.cancelInteractive()
|
||||
await native.replace(`/chat/${id}`, { presentation: 'push' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeDismissGesture as="main" class="screen sheet-screen">
|
||||
<div class="sheet-handle" aria-hidden="true" />
|
||||
<header class="sheet-header">
|
||||
<button type="button" @click="native.dismiss()">Cancel</button>
|
||||
<h1>New message</h1>
|
||||
<span />
|
||||
</header>
|
||||
<label class="search-field compose-search">
|
||||
<span>To:</span>
|
||||
<input v-model="query" autofocus placeholder="Search people" />
|
||||
</label>
|
||||
<div class="conversation-list">
|
||||
<button v-for="person in store.people.value.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))" :key="person.id" class="conversation-row" @click="choose(person.id)">
|
||||
<AppAvatar :person="person" />
|
||||
<div class="conversation-copy"><strong>{{ person.name }}</strong><p>{{ person.handle }}</p></div>
|
||||
<span class="chevron">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</NativeDismissGesture>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { useDemoStore } from '../data'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useDemoStore()
|
||||
const person = computed(() => store.personFor(String(route.params.id)))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="person" class="screen">
|
||||
<AppHeader title="Details" back />
|
||||
<section class="contact-hero">
|
||||
<AppAvatar :person="person" size="xl" />
|
||||
<h1>{{ person.name }}</h1>
|
||||
<p>{{ person.handle }}</p>
|
||||
<p>{{ person.bio }}</p>
|
||||
</section>
|
||||
<section class="contact-actions">
|
||||
<button><span>◌</span>Audio</button>
|
||||
<button><span>▣</span>Video</button>
|
||||
<button><span>⌕</span>Search</button>
|
||||
</section>
|
||||
<section class="settings-list">
|
||||
<button><span>▧</span><strong>Shared media</strong><i>24</i></button>
|
||||
<button><span>♧</span><strong>Mute notifications</strong><i>Off</i></button>
|
||||
<button class="danger"><span>⊘</span><strong>Block contact</strong><i /></button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { NativeGestureLink, useNativeRouter } from '@native-vue-router/core'
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { relativeTime, useDemoStore } from '../data'
|
||||
|
||||
const store = useDemoStore()
|
||||
const native = useNativeRouter()
|
||||
const query = ref('')
|
||||
const filtered = computed(() => store.conversations.value.filter((conversation) => {
|
||||
const person = store.personFor(conversation.personId)
|
||||
return person?.name.toLowerCase().includes(query.value.toLowerCase())
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="screen screen--tabs">
|
||||
<AppHeader title="Messages" subtitle="Thursday, 16 July" large>
|
||||
<button class="round-button" type="button" aria-label="Compose" @click="native.present('/compose', 'sheet')">+</button>
|
||||
</AppHeader>
|
||||
|
||||
<section class="content-stack">
|
||||
<label class="search-field">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<input v-model="query" type="search" placeholder="Search conversations" />
|
||||
</label>
|
||||
|
||||
<div class="story-strip" aria-label="Online friends">
|
||||
<div v-for="person in store.people.value.filter((item) => item.online)" :key="person.id" class="story-person">
|
||||
<AppAvatar :person="person" size="lg" />
|
||||
<span>{{ person.name.split(' ')[0] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">
|
||||
<h2>Recent</h2>
|
||||
<span>Drag a conversation left</span>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<NativeGestureLink
|
||||
v-for="conversation in filtered"
|
||||
:key="conversation.id"
|
||||
:to="`/chat/${conversation.id}`"
|
||||
presentation="reveal"
|
||||
direction="left"
|
||||
class="conversation-row"
|
||||
>
|
||||
<template v-if="store.personFor(conversation.personId)" >
|
||||
<AppAvatar :person="store.personFor(conversation.personId)!" size="md" />
|
||||
<div class="conversation-copy">
|
||||
<div>
|
||||
<strong>{{ store.personFor(conversation.personId)?.name }}</strong>
|
||||
<time>{{ relativeTime(conversation.messages.at(-1)?.sentAt ?? 0) }}</time>
|
||||
</div>
|
||||
<p>{{ conversation.messages.at(-1)?.body }}</p>
|
||||
</div>
|
||||
<span v-if="conversation.unread" class="unread-badge">{{ conversation.unread }}</span>
|
||||
<span v-else class="chevron" aria-hidden="true">›</span>
|
||||
</template>
|
||||
</NativeGestureLink>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { NativeLink } from '@native-vue-router/core'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="screen screen--tabs">
|
||||
<AppHeader title="You" subtitle="Your space" large />
|
||||
<section class="profile-card">
|
||||
<div class="profile-avatar">HV</div>
|
||||
<h2>Harvey</h2>
|
||||
<p>@harvmaster · Sydney</p>
|
||||
<div class="profile-stats">
|
||||
<div><strong>28</strong><span>Friends</span></div>
|
||||
<div><strong>164</strong><span>Moments</span></div>
|
||||
<div><strong>12</strong><span>Groups</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-list">
|
||||
<NativeLink to="/settings"><span>⚙︎</span><strong>Navigation lab</strong><i>›</i></NativeLink>
|
||||
<a href="https://github.com" target="_blank" rel="noreferrer"><span>⌘</span><strong>Project source</strong><i>↗</i></a>
|
||||
<button type="button"><span>◐</span><strong>Appearance</strong><i>System</i></button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { useDemoStore } from '../data'
|
||||
|
||||
const store = useDemoStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="screen">
|
||||
<AppHeader title="Navigation lab" subtitle="Runtime controls" back />
|
||||
<section class="lab-intro">
|
||||
<span>60</span>
|
||||
<div><strong>FPS baseline</strong><p>Interactive layers use transform and opacity only.</p></div>
|
||||
</section>
|
||||
<section class="settings-group">
|
||||
<h2>Simulation</h2>
|
||||
<label><span><strong>Network latency</strong><small>{{ store.settings.simulatedLatency }} ms</small></span><input v-model.number="store.settings.simulatedLatency" type="range" min="0" max="1200" step="20" /></label>
|
||||
<label><span><strong>Force message failures</strong><small>Test optimistic UI</small></span><input v-model="store.settings.simulateFailures" type="checkbox" /></label>
|
||||
<div><span><strong>Connection</strong><small>Browser network state</small></span><b :class="{ offline: store.offline.value }">{{ store.offline.value ? 'Offline' : 'Online' }}</b></div>
|
||||
</section>
|
||||
<section class="settings-group">
|
||||
<h2>Gesture recipes</h2>
|
||||
<p>Hold an edge-back gesture at any progress, swipe between primary routes, drag a conversation, or open the compose sheet.</p>
|
||||
</section>
|
||||
<button class="reset-button" type="button" @click="store.reset">Reset offline demo data</button>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import AppAvatar from '../components/AppAvatar.vue'
|
||||
import AppHeader from '../components/AppHeader.vue'
|
||||
import { useDemoStore } from '../data'
|
||||
|
||||
const store = useDemoStore()
|
||||
const gradients = [
|
||||
'linear-gradient(155deg, #f4a261, #d64e7d 48%, #5427a8)',
|
||||
'linear-gradient(155deg, #44c6e9, #4378db 48%, #1b245d)',
|
||||
'linear-gradient(155deg, #68d391, #2a9d8f 48%, #173e48)',
|
||||
'linear-gradient(155deg, #b794f4, #805ad5 48%, #322659)',
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="screen screen--tabs">
|
||||
<AppHeader title="Stories" subtitle="Moments from your circle" large />
|
||||
<section class="story-grid">
|
||||
<article v-for="(person, index) in store.people.value.slice(0, 4)" :key="person.id" class="story-card" :style="{ background: gradients[index] }">
|
||||
<div class="story-card__glow" />
|
||||
<AppAvatar :person="person" size="sm" />
|
||||
<div>
|
||||
<strong>{{ person.name }}</strong>
|
||||
<p>{{ index % 2 ? '2 hours ago' : 'Just now' }}</p>
|
||||
</div>
|
||||
<span class="story-card__mark">{{ ['✦', '◌', '△', '◇'][index] }}</span>
|
||||
</article>
|
||||
</section>
|
||||
<p class="gesture-tip">Swipe horizontally anywhere to move between primary routes.</p>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user