tangled
alpha
login
or
join now
vt3e.cat
/
bbell
7
fork
atom
wip bsky client for the web & android
bbell.vt3e.cat
7
fork
atom
overview
issues
pulls
pipelines
feat: split out app.vue into shell & splash; pronouns prompt
vt3e.cat
1 month ago
9ea4d2de
cbf5059a
verified
This commit was signed with the committer's
known signature
.
vt3e.cat
SSH Key Fingerprint:
SHA256:MaVgF6bXxDdD131G4rXizPh+sttp3IVsdPrj48HV0X0=
+476
-179
5 changed files
expand all
collapse all
unified
split
src
App.vue
components
Layout
AppShell.vue
SplashScreen.vue
stores
auth.ts
utils
keys.ts
+193
-178
src/App.vue
···
1
1
<script setup lang="ts">
2
2
-
import { onMounted, computed, ref, onUnmounted } from 'vue'
2
2
+
import { ref, onMounted, watch } from 'vue'
3
3
import { App, type URLOpenListenerEvent } from '@capacitor/app'
4
4
5
5
import { useNavigationStore } from './stores/navigation'
···
7
7
import { useThemeStore } from './stores/theme'
8
8
import { useAuthStore } from './stores/auth'
9
9
10
10
-
import TabStack from '@/components/Navigation/TabStack.vue'
11
11
-
import NavigationBar from '@/components/Navigation/NavigationBar.vue'
10
10
+
import SplashScreen from '@/components/Layout/SplashScreen.vue'
12
11
import OAuthCallback from '@/views/Auth/OAuthCallback.vue'
13
12
import OnboardingFlow from '@/views/Onboarding/OnboardingFlow.vue'
14
14
-
import PostComposer from '@/components/Composer/PostComposer.vue'
13
13
+
import AppShell from '@/components/Layout/AppShell.vue'
15
14
16
16
-
import { stackRoots, type StackRootNames } from './router'
17
15
import BaseModal from './components/UI/BaseModal.vue'
16
16
+
import TextInput from './components/UI/TextInput.vue'
17
17
+
import BaseButton from './components/UI/BaseButton.vue'
18
18
+
import KEYS from './utils/keys'
19
19
+
import type { AppBskyActorProfile } from '@atcute/bluesky'
20
20
+
import { ok } from '@atcute/client'
21
21
+
22
22
+
type AppPhase = 'loading' | 'callback' | 'intro' | 'shell'
23
23
+
const currentPhase = ref<AppPhase>('loading')
18
24
19
25
const nav = useNavigationStore()
20
26
const env = useEnvironmentStore()
21
27
const theme = useThemeStore()
22
28
const auth = useAuthStore()
23
29
24
24
-
const activeTab = computed(() => nav.activeTab)
25
25
-
const tabs: StackRootNames[] = stackRoots.map((p) => p.name)
30
30
+
// init stuff
31
31
+
// ========================================================
32
32
+
async function initializeApp() {
33
33
+
theme.init()
34
34
+
env.init()
35
35
+
auth.init()
36
36
+
37
37
+
const path = window.location.pathname
38
38
+
if (path.includes('/oauth/callback')) {
39
39
+
currentPhase.value = 'callback'
40
40
+
return
41
41
+
}
42
42
+
43
43
+
const wait = () => new Promise((resolve) => setTimeout(resolve, 750))
44
44
+
45
45
+
// waiting for auth
46
46
+
// we then either determine the Next Phase - either the onboarding flow or to the shell
47
47
+
if (auth.isLoading) {
48
48
+
const unwatch = watch(
49
49
+
() => auth.isLoading,
50
50
+
async (loading) => {
51
51
+
if (!loading) {
52
52
+
await wait()
53
53
+
unwatch()
54
54
+
determineNextPhase()
55
55
+
}
56
56
+
},
57
57
+
)
58
58
+
} else {
59
59
+
await wait()
60
60
+
determineNextPhase()
61
61
+
}
62
62
+
}
26
63
27
27
-
const isCallback = ref(window.location.pathname.includes('/oauth/callback'))
64
64
+
function determineNextPhase() {
65
65
+
const hasSeenIntro = localStorage.getItem(KEYS.STATE.INTRO_COMPLETE) === 'true'
28
66
29
29
-
const hasSeenIntro = localStorage.getItem('bluebell-intro-complete') === 'true'
30
30
-
const showIntro = ref(!hasSeenIntro && !isCallback.value)
67
67
+
// onboarding flow!
68
68
+
if (!hasSeenIntro) {
69
69
+
currentPhase.value = 'intro'
70
70
+
}
71
71
+
// shell/main app!
72
72
+
else {
73
73
+
finishStartup()
74
74
+
}
75
75
+
}
31
76
32
32
-
const modalOpen = computed(() => showPostComposerDialog.value)
33
33
-
const showPostComposerDialog = ref(false)
77
77
+
function finishStartup() {
78
78
+
nav.init()
79
79
+
currentPhase.value = 'shell'
34
80
35
35
-
auth.init()
81
81
+
const profile = auth.profile
82
82
+
if (!profile?.pronouns) showWokeModal.value = !localStorage.getItem(KEYS.STATE.WOKE_DISMISSED)
83
83
+
}
36
84
37
37
-
const onAuthComplete = () => {
85
85
+
// event handlers
86
86
+
// ========================================================
87
87
+
const onAuthCallbackComplete = () => {
38
88
window.history.replaceState(null, '', '/')
39
89
theme.init()
40
90
env.init()
41
41
-
auth.init()
42
42
-
isCallback.value = false
91
91
+
finishStartup()
43
92
}
44
93
45
94
const onIntroComplete = (action: 'stay' | 'login') => {
46
46
-
localStorage.setItem('bluebell-intro-complete', 'true')
47
47
-
showIntro.value = false
48
48
-
nav.init()
95
95
+
localStorage.setItem(KEYS.STATE.INTRO_COMPLETE, 'true')
96
96
+
finishStartup()
49
97
50
98
if (action === 'login') {
51
99
setTimeout(() => {
···
54
102
}
55
103
}
56
104
57
57
-
const handleBackNavigation = () => {
58
58
-
if (!nav.canGoBack) {
59
59
-
if (activeTab.value !== 'home') {
60
60
-
nav.switchTab('home')
61
61
-
return
62
62
-
}
63
63
-
App.exitApp().catch(() => {})
64
64
-
}
65
65
-
nav.pop()
66
66
-
}
67
67
-
68
68
-
function isTypingInInput(): boolean {
69
69
-
const active = document.activeElement as HTMLElement | null
70
70
-
if (!active) return false
71
71
-
return (
72
72
-
active.tagName === 'INPUT' ||
73
73
-
active.tagName === 'TEXTAREA' ||
74
74
-
active.tagName === 'SELECT' ||
75
75
-
active.isContentEditable
76
76
-
)
77
77
-
}
78
78
-
function handleKeybings(e: KeyboardEvent) {
79
79
-
if (isTypingInInput()) return
80
80
-
81
81
-
if (!e.ctrlKey) {
82
82
-
switch (e.key) {
83
83
-
case 'c':
84
84
-
showPostComposerDialog.value = true
85
85
-
break
86
86
-
case 'Escape':
87
87
-
showPostComposerDialog.value = false
88
88
-
break
89
89
-
}
90
90
-
}
91
91
-
}
92
92
-
93
93
-
onMounted(async () => {
94
94
-
App.addListener('backButton', handleBackNavigation)
95
95
-
document.addEventListener('keyup', (e) => {
96
96
-
if (e.key === 'e' && e.altKey) handleBackNavigation()
97
97
-
})
105
105
+
// ========================================================
106
106
+
onMounted(() => {
107
107
+
initializeApp()
98
108
99
109
App.addListener('appUrlOpen', function (event: URLOpenListenerEvent) {
100
110
const url = new URL(event.url)
···
102
112
const hash = url.hash
103
113
104
114
if (!path) return
115
115
+
105
116
if (path.startsWith('/oauth/callback')) {
106
117
auth._hash = hash
107
107
-
isCallback.value = true
118
118
+
currentPhase.value = 'callback'
108
119
} else {
109
120
nav.navigateToUrl(path)
110
121
}
111
122
})
123
123
+
})
112
124
113
113
-
window.addEventListener('keyup', handleKeybings)
125
125
+
// woke helpers
126
126
+
// ========================================================
127
127
+
const pronounsError = ref('')
128
128
+
const pronouns = ref('')
129
129
+
const showWokeModal = ref(false)
130
130
+
const updatingPronouns = ref(false)
131
131
+
const suggestedPronouns = ['she/her', 'they/them', 'he/him', 'any']
132
132
+
133
133
+
function handlePronounsChange(value: string) {
134
134
+
pronouns.value = value
135
135
+
}
114
136
115
115
-
theme.init()
116
116
-
env.init()
137
137
+
async function savePronouns() {
138
138
+
try {
139
139
+
updatingPronouns.value = true
140
140
+
141
141
+
const rpc = auth.getRpc()
142
142
+
const did = auth.userDid
143
143
+
if (!did) return
117
144
118
118
-
if (!showIntro.value && !isCallback.value) {
119
119
-
nav.init()
145
145
+
const profile = ok(
146
146
+
await rpc.get('com.atproto.repo.getRecord', {
147
147
+
params: {
148
148
+
collection: 'app.bsky.actor.profile',
149
149
+
repo: did,
150
150
+
rkey: 'self',
151
151
+
},
152
152
+
}),
153
153
+
)
154
154
+
155
155
+
await rpc.post('com.atproto.repo.putRecord', {
156
156
+
input: {
157
157
+
collection: 'app.bsky.actor.profile',
158
158
+
repo: auth.userDid,
159
159
+
rkey: 'self',
160
160
+
record: {
161
161
+
...(profile.value as AppBskyActorProfile.Main),
162
162
+
pronouns: pronouns.value,
163
163
+
} as AppBskyActorProfile.Main,
164
164
+
},
165
165
+
})
166
166
+
167
167
+
localStorage.removeItem(KEYS.STATE.WOKE_DISMISSED)
168
168
+
showWokeModal.value = false
169
169
+
} catch (err) {
170
170
+
if (err instanceof Error) pronounsError.value = err.message
171
171
+
} finally {
172
172
+
updatingPronouns.value = false
120
173
}
121
121
-
})
174
174
+
}
122
175
123
123
-
onUnmounted(() => {
124
124
-
App.removeAllListeners()
125
125
-
window.removeEventListener('keyup', handleKeybings)
126
126
-
})
176
176
+
function remindLater() {
177
177
+
localStorage.setItem(KEYS.STATE.WOKE_DISMISSED, new Date().toISOString())
178
178
+
showWokeModal.value = false
179
179
+
}
127
180
</script>
128
181
129
182
<template>
130
130
-
<Transition name="intro-fade">
131
131
-
<OnboardingFlow v-if="showIntro" @complete="onIntroComplete('stay')" />
132
132
-
</Transition>
183
183
+
<div class="app-root">
184
184
+
<Transition name="fade" mode="out-in">
185
185
+
<SplashScreen v-if="currentPhase === 'loading'" key="loading" />
186
186
+
187
187
+
<OAuthCallback
188
188
+
v-else-if="currentPhase === 'callback'"
189
189
+
key="callback"
190
190
+
class="view-layer"
191
191
+
@complete="onAuthCallbackComplete"
192
192
+
/>
193
193
+
194
194
+
<OnboardingFlow
195
195
+
v-else-if="currentPhase === 'intro'"
196
196
+
key="intro"
197
197
+
@complete="onIntroComplete('stay')"
198
198
+
/>
199
199
+
200
200
+
<div
201
201
+
v-else-if="currentPhase === 'shell'"
202
202
+
key="shell"
203
203
+
class="shell-wrapper"
204
204
+
:inert="showWokeModal"
205
205
+
>
206
206
+
<AppShell />
207
207
+
</div>
208
208
+
</Transition>
133
209
134
134
-
<Transition name="app-fade" mode="in-out">
135
135
-
<div v-if="!showIntro" class="app-root" :inert="modalOpen">
136
136
-
<Transition name="app-fade" mode="out-in">
137
137
-
<OAuthCallback
138
138
-
v-if="isCallback"
139
139
-
@complete="onAuthComplete"
140
140
-
class="view-layer"
141
141
-
:key="'callback'"
210
210
+
<BaseModal title="Add your pronouns" :open="showWokeModal" width="600px">
211
211
+
<div class="woke-modal">
212
212
+
<p>Let people know how to refer to you!</p>
213
213
+
<TextInput
214
214
+
placeholder="e.g. she/her, they/them, he/him, any"
215
215
+
v-model="pronouns"
216
216
+
class="input-pronouns"
142
217
/>
143
143
-
<div v-else class="app-shell view-layer" :key="'shell'">
144
144
-
<div class="skip-links">
145
145
-
<a href="#main-content" id="skip-to-content" class="skip-link">
146
146
-
skip to main content
147
147
-
</a>
148
148
-
<a href="#navigation-bar" class="skip-link"> skip to navigation </a>
149
149
-
</div>
150
150
-
151
151
-
<div class="viewport" id="main-content">
152
152
-
<TabStack
153
153
-
v-for="t in tabs"
154
154
-
:key="t"
155
155
-
:tab="t"
156
156
-
v-show="activeTab === t"
157
157
-
:class="{ active: activeTab === t }"
158
158
-
/>
159
159
-
</div>
160
160
-
<NavigationBar ref="navBar" />
218
218
+
<div class="suggested-pronouns">
219
219
+
<BaseButton
220
220
+
variant="subtle-alt"
221
221
+
size="sm"
222
222
+
pill
223
223
+
v-for="pronoun in suggestedPronouns"
224
224
+
:key="pronoun"
225
225
+
@click="handlePronounsChange(pronoun)"
226
226
+
>
227
227
+
{{ pronoun }}
228
228
+
</BaseButton>
229
229
+
</div>
230
230
+
<div class="pronouns-error">
231
231
+
<p v-if="pronounsError">{{ pronounsError }}</p>
161
232
</div>
162
162
-
</Transition>
163
163
-
</div>
164
164
-
</Transition>
165
165
-
166
166
-
<BaseModal
167
167
-
title="New Post"
168
168
-
:open="showPostComposerDialog"
169
169
-
width="600px"
170
170
-
@close="showPostComposerDialog = false"
171
171
-
>
172
172
-
<PostComposer @close="showPostComposerDialog = false" />
173
173
-
</BaseModal>
233
233
+
</div>
234
234
+
<template #footer>
235
235
+
<BaseButton variant="subtle-alt" size="md" @click="remindLater">Maybe later</BaseButton>
236
236
+
<BaseButton :loading="updatingPronouns" :disabled="!pronouns" @click="savePronouns"
237
237
+
>Save</BaseButton
238
238
+
>
239
239
+
</template>
240
240
+
</BaseModal>
241
241
+
</div>
174
242
</template>
175
243
176
244
<style scoped>
···
186
254
height: 100vh;
187
255
}
188
256
189
189
-
.intro-fade-leave-active {
190
190
-
transition: opacity 0.8s ease;
191
191
-
}
192
192
-
.intro-fade-leave-to {
193
193
-
opacity: 0;
257
257
+
.shell-wrapper {
258
258
+
height: 100%;
259
259
+
width: 100%;
194
260
}
195
261
196
196
-
.app-fade-enter-active {
197
197
-
transition: opacity 1s ease 0.2s;
198
198
-
}
199
199
-
.app-fade-leave-active {
200
200
-
transition: opacity 0.5s ease;
262
262
+
.fade-enter-active,
263
263
+
.fade-leave-active {
264
264
+
transition: opacity 0.3s ease;
201
265
}
202
266
203
203
-
.app-fade-enter-from {
267
267
+
.fade-enter-from,
268
268
+
.fade-leave-to {
204
269
opacity: 0;
205
205
-
transform: scale(0.98);
206
270
}
207
271
208
208
-
.app-fade-leave-to {
209
209
-
opacity: 0;
272
272
+
.woke-modal .input-pronouns {
273
273
+
margin-top: 0.5rem;
210
274
}
211
211
-
212
212
-
.app-shell {
213
213
-
position: relative;
214
214
-
height: 100vh;
215
215
-
width: 100vw;
216
216
-
overflow: hidden;
217
217
-
background-color: hsla(var(--mantle) / 1);
275
275
+
.woke-modal .suggested-pronouns {
276
276
+
margin-top: -0.75rem;
218
277
display: flex;
219
219
-
flex-direction: column;
220
220
-
}
221
221
-
222
222
-
.viewport {
223
223
-
position: relative;
224
224
-
flex: 1;
225
225
-
overflow: hidden;
226
226
-
z-index: 0;
227
227
-
}
228
228
-
229
229
-
@media (min-width: 512px) {
230
230
-
.app-shell {
231
231
-
flex-direction: row;
232
232
-
justify-content: center;
233
233
-
}
234
234
-
235
235
-
.viewport {
236
236
-
flex: 1;
237
237
-
order: 2;
238
238
-
max-width: 768px;
239
239
-
}
240
240
-
}
241
241
-
242
242
-
.skip-links {
243
243
-
position: absolute;
244
244
-
top: -100px;
245
245
-
left: 0;
246
246
-
z-index: 1000;
247
247
-
}
248
248
-
249
249
-
.skip-link {
250
250
-
position: absolute;
251
251
-
top: -100px;
252
252
-
left: 8px;
253
253
-
background: hsl(var(--overlay0));
254
254
-
color: hsl(var(--blue));
255
255
-
padding: 8px 16px;
256
256
-
text-decoration: none;
257
257
-
border-radius: 4px;
258
258
-
font-weight: 600;
259
259
-
z-index: 11000;
260
260
-
&:focus {
261
261
-
top: 128px;
262
262
-
width: fit-content;
263
263
-
}
278
278
+
gap: 0.25rem;
264
279
}
265
280
</style>
+153
src/components/Layout/AppShell.vue
···
1
1
+
<script setup lang="ts">
2
2
+
import { computed, ref, onMounted, onUnmounted } from 'vue'
3
3
+
import { App } from '@capacitor/app'
4
4
+
import { useNavigationStore } from '@/stores/navigation'
5
5
+
import { stackRoots, type StackRootNames } from '@/router'
6
6
+
7
7
+
import TabStack from '@/components/Navigation/TabStack.vue'
8
8
+
import NavigationBar from '@/components/Navigation/NavigationBar.vue'
9
9
+
import PostComposer from '@/components/Composer/PostComposer.vue'
10
10
+
import BaseModal from '@/components/UI/BaseModal.vue'
11
11
+
12
12
+
const nav = useNavigationStore()
13
13
+
const activeTab = computed(() => nav.activeTab)
14
14
+
const tabs: StackRootNames[] = stackRoots.map((p) => p.name)
15
15
+
16
16
+
const showPostComposerDialog = ref(false)
17
17
+
18
18
+
const handleBackNavigation = () => {
19
19
+
if (!nav.canGoBack) {
20
20
+
if (activeTab.value !== 'home') {
21
21
+
nav.switchTab('home')
22
22
+
return
23
23
+
}
24
24
+
25
25
+
// exit if we are at root of home
26
26
+
App.exitApp().catch(() => {})
27
27
+
}
28
28
+
nav.pop()
29
29
+
}
30
30
+
31
31
+
function isTypingInInput(): boolean {
32
32
+
const active = document.activeElement as HTMLElement | null
33
33
+
if (!active) return false
34
34
+
return (
35
35
+
active.tagName === 'INPUT' ||
36
36
+
active.tagName === 'TEXTAREA' ||
37
37
+
active.tagName === 'SELECT' ||
38
38
+
active.isContentEditable
39
39
+
)
40
40
+
}
41
41
+
42
42
+
function handleKeybings(e: KeyboardEvent) {
43
43
+
if (isTypingInInput()) return
44
44
+
if (!e.ctrlKey) {
45
45
+
switch (e.key) {
46
46
+
case 'c':
47
47
+
showPostComposerDialog.value = true
48
48
+
break
49
49
+
case 'Escape':
50
50
+
showPostComposerDialog.value = false
51
51
+
break
52
52
+
}
53
53
+
}
54
54
+
}
55
55
+
56
56
+
onMounted(() => {
57
57
+
App.addListener('backButton', handleBackNavigation)
58
58
+
document.addEventListener('keyup', (e) => {
59
59
+
if (e.key === 'e' && e.altKey) handleBackNavigation()
60
60
+
})
61
61
+
window.addEventListener('keyup', handleKeybings)
62
62
+
})
63
63
+
64
64
+
onUnmounted(() => {
65
65
+
App.removeAllListeners()
66
66
+
window.removeEventListener('keyup', handleKeybings)
67
67
+
})
68
68
+
</script>
69
69
+
70
70
+
<template>
71
71
+
<div class="app-shell">
72
72
+
<div class="skip-links">
73
73
+
<a href="#main-content" id="skip-to-content" class="skip-link"> skip to main content </a>
74
74
+
<a href="#navigation-bar" class="skip-link"> skip to navigation </a>
75
75
+
</div>
76
76
+
77
77
+
<div class="viewport" id="main-content">
78
78
+
<TabStack
79
79
+
v-for="t in tabs"
80
80
+
:key="t"
81
81
+
:tab="t"
82
82
+
v-show="activeTab === t"
83
83
+
:class="{ active: activeTab === t }"
84
84
+
/>
85
85
+
</div>
86
86
+
<NavigationBar ref="navBar" />
87
87
+
88
88
+
<BaseModal
89
89
+
title="New Post"
90
90
+
:open="showPostComposerDialog"
91
91
+
width="600px"
92
92
+
@close="showPostComposerDialog = false"
93
93
+
>
94
94
+
<PostComposer @close="showPostComposerDialog = false" />
95
95
+
</BaseModal>
96
96
+
</div>
97
97
+
</template>
98
98
+
99
99
+
<style scoped>
100
100
+
.app-shell {
101
101
+
position: relative;
102
102
+
height: 100vh;
103
103
+
width: 100vw;
104
104
+
overflow: hidden;
105
105
+
background-color: hsla(var(--mantle) / 1);
106
106
+
display: flex;
107
107
+
flex-direction: column;
108
108
+
}
109
109
+
110
110
+
.viewport {
111
111
+
position: relative;
112
112
+
flex: 1;
113
113
+
overflow: hidden;
114
114
+
z-index: 0;
115
115
+
}
116
116
+
117
117
+
@media (min-width: 512px) {
118
118
+
.app-shell {
119
119
+
flex-direction: row;
120
120
+
justify-content: center;
121
121
+
}
122
122
+
123
123
+
.viewport {
124
124
+
flex: 1;
125
125
+
order: 2;
126
126
+
max-width: 668px;
127
127
+
}
128
128
+
}
129
129
+
130
130
+
.skip-links {
131
131
+
position: absolute;
132
132
+
top: -100px;
133
133
+
left: 0;
134
134
+
z-index: 1000;
135
135
+
}
136
136
+
137
137
+
.skip-link {
138
138
+
position: absolute;
139
139
+
top: -100px;
140
140
+
left: 8px;
141
141
+
background: hsl(var(--overlay0));
142
142
+
color: hsl(var(--blue));
143
143
+
padding: 8px 16px;
144
144
+
text-decoration: none;
145
145
+
border-radius: 4px;
146
146
+
font-weight: 600;
147
147
+
z-index: 11000;
148
148
+
&:focus {
149
149
+
top: 128px;
150
150
+
width: fit-content;
151
151
+
}
152
152
+
}
153
153
+
</style>
+128
src/components/Layout/SplashScreen.vue
···
1
1
+
<script setup lang="ts">
2
2
+
import SVG from '@/components/UI/SVG.vue'
3
3
+
import BluebellLogo from '@/assets/icons/bluebell.svg?raw'
4
4
+
</script>
5
5
+
6
6
+
<template>
7
7
+
<div class="splash-screen">
8
8
+
<div class="splash-content">
9
9
+
<div class="logo-wrapper">
10
10
+
<SVG :icon="BluebellLogo" class="logo" />
11
11
+
</div>
12
12
+
13
13
+
<div class="brand-text">
14
14
+
<h1 class="title">Bluebell</h1>
15
15
+
<div class="loader-track">
16
16
+
<div class="loader-bar"></div>
17
17
+
</div>
18
18
+
</div>
19
19
+
</div>
20
20
+
</div>
21
21
+
</template>
22
22
+
23
23
+
<style scoped lang="scss">
24
24
+
.splash-screen {
25
25
+
display: flex;
26
26
+
flex-direction: column;
27
27
+
align-items: center;
28
28
+
justify-content: center;
29
29
+
height: 100vh;
30
30
+
width: 100vw;
31
31
+
background-color: hsl(var(--base));
32
32
+
color: hsl(var(--text));
33
33
+
position: fixed;
34
34
+
inset: 0;
35
35
+
z-index: 9999;
36
36
+
}
37
37
+
38
38
+
.splash-content {
39
39
+
display: flex;
40
40
+
flex-direction: column;
41
41
+
align-items: center;
42
42
+
gap: 2rem;
43
43
+
margin-bottom: 10vh;
44
44
+
}
45
45
+
46
46
+
.logo-wrapper {
47
47
+
width: 6rem;
48
48
+
height: 6rem;
49
49
+
color: hsl(var(--accent));
50
50
+
51
51
+
:deep(svg) {
52
52
+
width: 100%;
53
53
+
height: 100%;
54
54
+
}
55
55
+
}
56
56
+
57
57
+
.brand-text {
58
58
+
display: flex;
59
59
+
flex-direction: column;
60
60
+
align-items: center;
61
61
+
gap: 1rem;
62
62
+
}
63
63
+
64
64
+
.title {
65
65
+
font-size: 2.5rem;
66
66
+
font-weight: 800;
67
67
+
letter-spacing: -0.03em;
68
68
+
background: linear-gradient(135deg, hsl(var(--text)) 0%, hsl(var(--subtext0)) 100%);
69
69
+
background-clip: text;
70
70
+
-webkit-text-fill-color: transparent;
71
71
+
margin: 0;
72
72
+
}
73
73
+
74
74
+
.loader-track {
75
75
+
width: 120px;
76
76
+
height: 4px;
77
77
+
background-color: hsla(var(--surface2) / 0.3);
78
78
+
border-radius: 99px;
79
79
+
overflow: hidden;
80
80
+
position: relative;
81
81
+
}
82
82
+
83
83
+
.loader-bar {
84
84
+
position: absolute;
85
85
+
top: 0;
86
86
+
left: 0;
87
87
+
height: 100%;
88
88
+
width: 100%;
89
89
+
background-color: hsl(var(--accent));
90
90
+
border-radius: 99px;
91
91
+
transform-origin: left;
92
92
+
animation: loading 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite;
93
93
+
}
94
94
+
95
95
+
.footer {
96
96
+
position: absolute;
97
97
+
bottom: 2rem;
98
98
+
p {
99
99
+
font-size: 0.875rem;
100
100
+
color: hsl(var(--subtext0));
101
101
+
opacity: 0.6;
102
102
+
font-weight: 500;
103
103
+
letter-spacing: 0.02em;
104
104
+
}
105
105
+
}
106
106
+
107
107
+
@keyframes float {
108
108
+
0%,
109
109
+
100% {
110
110
+
transform: translateY(0);
111
111
+
}
112
112
+
50% {
113
113
+
transform: translateY(-10px);
114
114
+
}
115
115
+
}
116
116
+
117
117
+
@keyframes loading {
118
118
+
0% {
119
119
+
transform: translateX(-100%);
120
120
+
}
121
121
+
50% {
122
122
+
transform: translateX(0);
123
123
+
}
124
124
+
100% {
125
125
+
transform: translateX(100%);
126
126
+
}
127
127
+
}
128
128
+
</style>
+1
src/stores/auth.ts
···
83
83
84
84
async function fetchProfile() {
85
85
if (!session.value) return
86
86
+
86
87
try {
87
88
const rpc = getRpc()
88
89
const data = await ok(
+1
-1
src/utils/keys.ts
···
20
20
'ACCENT_COLOUR',
21
21
]),
22
22
23
23
-
STATE: defineScope('state', ['ACTIVE_FEED_URI']),
23
23
+
STATE: defineScope('state', ['ACTIVE_FEED_URI', 'WOKE_DISMISSED', 'INTRO_COMPLETE']),
24
24
SETTINGS: defineScope('settings', ['BASE']),
25
25
AUTH: defineScope('auth', ['SESSION', 'ACTIVE_DID']),
26
26
}