+30
src/app.css
+30
src/app.css
···
95
95
box-shadow: 0 0 20px 5px var(--nucleus-selected-post);
96
96
}
97
97
}
98
+
99
+
@keyframes slide-in-from-right {
100
+
from {
101
+
transform: translateX(144px);
102
+
opacity: 0;
103
+
}
104
+
to {
105
+
transform: translateX(0);
106
+
opacity: 1;
107
+
}
108
+
}
109
+
110
+
@keyframes slide-in-from-left {
111
+
from {
112
+
transform: translateX(-144px);
113
+
opacity: 0;
114
+
}
115
+
to {
116
+
transform: translateX(0);
117
+
opacity: 1;
118
+
}
119
+
}
120
+
121
+
.animate-slide-in-right {
122
+
animation: slide-in-from-right 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
123
+
}
124
+
125
+
.animate-slide-in-left {
126
+
animation: slide-in-from-left 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
127
+
}
-30
src/components/NotificationsPopup.svelte
-30
src/components/NotificationsPopup.svelte
···
1
-
<script lang="ts">
2
-
import Popup from './Popup.svelte';
3
-
4
-
interface Props {
5
-
isOpen: boolean;
6
-
onClose: () => void;
7
-
}
8
-
9
-
let { isOpen = $bindable(false), onClose }: Props = $props();
10
-
11
-
const handleClose = () => {
12
-
onClose();
13
-
};
14
-
</script>
15
-
16
-
<Popup
17
-
bind:isOpen
18
-
onClose={handleClose}
19
-
title="notifications"
20
-
width="w-[42vmax] max-w-2xl"
21
-
height="60vh"
22
-
showHeaderDivider={true}
23
-
>
24
-
<div class="flex h-full items-center justify-center">
25
-
<div class="text-center">
26
-
<div class="mb-4 text-6xl opacity-50">🚧</div>
27
-
<h3 class="text-xl font-bold opacity-80">todo</h3>
28
-
</div>
29
-
</div>
30
-
</Popup>
+18
src/components/NotificationsView.svelte
+18
src/components/NotificationsView.svelte
···
1
+
<div class="p-4">
2
+
<div class="mb-6">
3
+
<h2 class="text-3xl font-bold">notifications</h2>
4
+
<div class="mt-2 flex gap-2">
5
+
<div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div>
6
+
<div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div>
7
+
</div>
8
+
</div>
9
+
10
+
<div
11
+
class="flex h-64 items-center justify-center rounded-sm border-2 border-dashed border-(--nucleus-fg)/10"
12
+
>
13
+
<div class="text-center">
14
+
<div class="mb-4 text-6xl opacity-50">🚧</div>
15
+
<h3 class="text-xl font-bold opacity-80">todo</h3>
16
+
</div>
17
+
</div>
18
+
</div>
+2
-1
src/components/RichText.svelte
+2
-1
src/components/RichText.svelte
···
41
41
>
42
42
{:else if feature.$type === 'app.bsky.richtext.facet#link'}
43
43
{@const uri = new URL(feature.uri)}
44
+
{@const text = `${!uri.protocol.startsWith('http') ? `${uri.protocol}//` : ''}${uri.host}${uri.hash.length === 0 && uri.search.length === 0 && uri.pathname === '/' ? '' : uri.pathname}${uri.search}${uri.hash}`}
44
45
<a
45
46
class="text-(--nucleus-accent2)"
46
47
href={uri.href}
47
48
target="_blank"
48
49
rel="noopener noreferrer"
49
-
>{@render plainText(uri.href.replace(`${uri.protocol}//`, ''))}</a
50
+
>{@render plainText(`${text.substring(0, 40)}${text.length > 40 ? '...' : ''}`)}</a
50
51
>
51
52
{:else if feature.$type === 'app.bsky.richtext.facet#tag'}
52
53
<a
+63
-69
src/components/SettingsPopup.svelte
src/components/SettingsView.svelte
+63
-69
src/components/SettingsPopup.svelte
src/components/SettingsView.svelte
···
3
3
import { handleCache, didDocCache, recordCache } from '$lib/at/client';
4
4
import { get } from 'svelte/store';
5
5
import ColorPicker from 'svelte-awesome-color-picker';
6
-
import Popup from './Popup.svelte';
7
6
import Tabs from './Tabs.svelte';
8
-
9
-
interface Props {
10
-
isOpen: boolean;
11
-
onClose: () => void;
12
-
}
13
-
14
-
let { isOpen = $bindable(false), onClose }: Props = $props();
7
+
import { portal } from 'svelte-portal';
15
8
16
9
type Tab = 'style' | 'moderation' | 'advanced';
17
10
let activeTab = $state<Tab>('advanced');
···
23
16
$settings.theme = localSettings.theme;
24
17
});
25
18
26
-
const resetSettingsToSaved = () => {
27
-
localSettings = $settings;
28
-
};
29
-
30
-
const handleClose = () => {
31
-
resetSettingsToSaved();
32
-
onClose();
33
-
};
34
-
35
19
const handleSave = () => {
36
20
settings.set(localSettings);
37
21
window.location.reload();
···
56
40
<div class="h-px bg-linear-to-r from-(--nucleus-accent) to-(--nucleus-accent2)"></div>
57
41
{/snippet}
58
42
59
-
{#snippet settingHeader(name: string, desc: string)}
60
-
<h3 class="mb-3 text-lg font-bold">{name}</h3>
61
-
<p class="mb-4 text-sm opacity-80">{desc}</p>
62
-
{/snippet}
63
-
64
43
{#snippet advancedTab()}
65
-
<div class="space-y-5">
44
+
<div class="space-y-3 p-4">
66
45
<div>
67
-
<h3 class="mb-3 text-lg font-bold">api endpoints</h3>
68
-
<div class="space-y-4">
46
+
<h3 class="header">api endpoints</h3>
47
+
<div class="borders space-y-4">
69
48
{#snippet _input(name: string, desc: string)}
70
49
<div>
71
-
<label for={name} class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
50
+
<label for={name} class="header-desc block">
72
51
{desc}
73
52
</label>
74
53
<input
···
86
65
</div>
87
66
</div>
88
67
89
-
{@render divider()}
90
-
91
-
<div>
68
+
<div class="borders">
92
69
<label for="social-app-url" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
93
70
social-app url (for when copying links to posts / profiles)
94
71
</label>
···
101
78
/>
102
79
</div>
103
80
104
-
{@render divider()}
105
-
106
-
<div>
107
-
{@render settingHeader(
108
-
'cache management',
109
-
'clears cached data (records, DID documents, handles, etc.)'
110
-
)}
81
+
<h3 class="header">cache management</h3>
82
+
<div class="borders">
83
+
<p class="header-desc">clears cached data (records, DID documents, handles, etc.)</p>
111
84
<button onclick={handleClearCache} class="action-button"> clear cache </button>
112
85
</div>
113
86
114
-
{@render divider()}
115
-
116
-
<div>
117
-
{@render settingHeader('reset settings', 'resets all settings to their default values')}
87
+
<h3 class="header">reset settings</h3>
88
+
<div class="borders">
89
+
<p class="header-desc">resets all settings to their default values</p>
118
90
<button
119
91
onclick={handleReset}
120
92
class="action-button border-red-600 text-red-600 hover:bg-red-600/20"
···
126
98
{/snippet}
127
99
128
100
{#snippet styleTab()}
129
-
<div class="space-y-5">
101
+
<div class="space-y-5 p-4">
130
102
<div>
131
-
<h3 class="mb-3 text-lg font-bold">colors</h3>
132
-
<div class="space-y-4">
103
+
<h3 class="header">colors</h3>
104
+
<div class="borders">
133
105
{#snippet color(name: string, desc: string)}
134
106
<div>
135
-
<label for={name} class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
107
+
<label for={name} class="header-desc block">
136
108
{desc}
137
109
</label>
138
110
<div class="color-picker">
···
154
126
</div>
155
127
{/snippet}
156
128
157
-
<Popup
158
-
bind:isOpen
159
-
onClose={handleClose}
160
-
title="settings"
161
-
width="w-[42vmax] max-w-2xl"
162
-
height="60vh"
163
-
showHeaderDivider={true}
164
-
>
165
-
{#snippet headerActions()}
129
+
<div class="flex flex-col">
130
+
<div class="mb-6 flex items-center justify-between p-4 pb-0">
131
+
<div>
132
+
<h2 class="text-3xl font-bold">settings</h2>
133
+
<div class="mt-2 flex gap-2">
134
+
<div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div>
135
+
<div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div>
136
+
</div>
137
+
</div>
166
138
{#if hasReloadChanges}
167
-
<button onclick={handleSave} class="shrink-0 action-button"> save & reload </button>
139
+
<button onclick={handleSave} class="action-button animate-pulse shadow-lg">
140
+
save & reload
141
+
</button>
168
142
{/if}
169
-
{/snippet}
143
+
</div>
170
144
171
-
{#if activeTab === 'advanced'}
172
-
{@render advancedTab()}
173
-
{:else if activeTab === 'moderation'}
174
-
<div class="flex h-full items-center justify-center">
175
-
<div class="text-center">
176
-
<div class="mb-4 text-6xl opacity-50">🚧</div>
177
-
<h3 class="text-xl font-bold opacity-80">todo</h3>
145
+
<div class="flex-1">
146
+
{#if activeTab === 'advanced'}
147
+
{@render advancedTab()}
148
+
{:else if activeTab === 'moderation'}
149
+
<div class="p-4">
150
+
<div class="flex h-64 items-center justify-center">
151
+
<div class="text-center">
152
+
<div class="mb-4 text-6xl opacity-50">🚧</div>
153
+
<h3 class="text-xl font-bold opacity-80">todo</h3>
154
+
</div>
155
+
</div>
178
156
</div>
179
-
</div>
180
-
{:else if activeTab === 'style'}
181
-
{@render styleTab()}
182
-
{/if}
157
+
{:else if activeTab === 'style'}
158
+
{@render styleTab()}
159
+
{/if}
160
+
</div>
183
161
184
-
{#snippet footer()}
162
+
<div
163
+
use:portal={'#app-footer'}
164
+
class="fixed bottom-[5dvh] z-20 w-full max-w-2xl p-4 pt-2 shadow-[0_-10px_20px_-5px_rgba(0,0,0,0.1)]"
165
+
>
185
166
<Tabs
186
167
tabs={['style', 'moderation', 'advanced']}
187
168
bind:activeTab
188
169
onTabChange={(tab) => (activeTab = tab)}
189
170
/>
190
-
{/snippet}
191
-
</Popup>
171
+
</div>
172
+
</div>
173
+
174
+
<style>
175
+
@reference "../app.css";
176
+
.borders {
177
+
@apply rounded-sm border-2 border-dashed border-(--nucleus-fg)/10 p-4;
178
+
}
179
+
.header-desc {
180
+
@apply mb-2 text-sm text-(--nucleus-fg)/80;
181
+
}
182
+
.header {
183
+
@apply mb-2 text-lg font-bold;
184
+
}
185
+
</style>
+6
-4
src/components/Tabs.svelte
+6
-4
src/components/Tabs.svelte
···
8
8
let { tabs, activeTab = $bindable(), onTabChange }: Props = $props();
9
9
</script>
10
10
11
-
<div class="flex">
12
-
{#each tabs as tab (tab)}
11
+
<div class="flex rounded border-x-3 border-b-3 border-(--nucleus-accent)/20">
12
+
{#each tabs as tab, idx (tab)}
13
13
{@const isActive = activeTab === tab}
14
14
<button
15
15
onclick={() => onTabChange(tab)}
16
-
class="flex-1 border-t-3 px-4 py-3 font-semibold transition-colors hover:cursor-pointer {isActive
17
-
? 'border-(--nucleus-accent) bg-(--nucleus-accent)/20 text-(--nucleus-accent)'
16
+
class="flex-1 border-t-3 px-4 py-3
17
+
font-semibold transition-colors hover:cursor-pointer
18
+
{isActive
19
+
? 'rounded-t border-(--nucleus-accent) bg-(--nucleus-accent)/20 text-(--nucleus-accent)'
18
20
: 'border-(--nucleus-accent)/20 bg-transparent text-(--nucleus-fg)/60 hover:bg-(--nucleus-accent)/10'}"
19
21
>
20
22
{tab}
+123
-94
src/routes/+page.svelte
+123
-94
src/routes/+page.svelte
···
2
2
import BskyPost from '$components/BskyPost.svelte';
3
3
import PostComposer, { type State as PostComposerState } from '$components/PostComposer.svelte';
4
4
import AccountSelector from '$components/AccountSelector.svelte';
5
-
import SettingsPopup from '$components/SettingsPopup.svelte';
5
+
import SettingsView from '$components/SettingsView.svelte';
6
+
import NotificationsView from '$components/NotificationsView.svelte';
6
7
import { AtpClient, type NotificationsStreamEvent } from '$lib/at/client';
7
8
import { accounts, type Account } from '$lib/accounts';
8
9
import { type Did, parseCanonicalResourceUri, type ResourceUri } from '@atcute/lexicons';
9
-
import { onMount } from 'svelte';
10
+
import { onMount, tick } from 'svelte';
10
11
import { fetchPostsWithBacklinks, hydratePosts, type PostWithUri } from '$lib/at/fetch';
11
12
import { expect } from '$lib/result';
12
13
import { AppBskyFeedPost } from '@atcute/bluesky';
···
19
20
import type { AtprotoDid } from '@atcute/lexicons/syntax';
20
21
import type { PageProps } from './+page';
21
22
import { buildThreads, filterThreads, type ThreadPost } from '$lib/thread';
22
-
import NotificationsPopup from '$components/NotificationsPopup.svelte';
23
23
24
24
const { data: loadData }: PageProps = $props();
25
25
···
66
66
handleAccountSelected(newAccounts[0]?.did);
67
67
};
68
68
69
-
let isSettingsOpen = $state(false);
70
-
let isNotificationsOpen = $state(false);
69
+
type View = 'timeline' | 'notifications' | 'settings';
70
+
let currentView = $state<View>('timeline');
71
+
let animClass = $state('animate-fade-in-scale');
72
+
let timelineScrollPosition = $state(0);
73
+
74
+
const viewOrder: Record<View, number> = {
75
+
timeline: 0,
76
+
notifications: 1,
77
+
settings: 2
78
+
};
79
+
80
+
const switchView = async (newView: View) => {
81
+
if (currentView === newView) return;
82
+
if (currentView === 'timeline') timelineScrollPosition = window.scrollY;
83
+
84
+
const direction = viewOrder[newView] > viewOrder[currentView] ? 'right' : 'left';
85
+
animClass = direction === 'right' ? 'animate-slide-in-right' : 'animate-slide-in-left';
86
+
currentView = newView;
87
+
88
+
await tick();
89
+
90
+
if (newView !== 'timeline') window.scrollTo({ top: 0, behavior: 'instant' });
91
+
else window.scrollTo({ top: timelineScrollPosition, behavior: 'instant' });
92
+
};
93
+
71
94
let reverseChronological = $state(true);
72
95
let viewOwnPosts = $state(true);
73
96
···
152
175
}
153
176
};
154
177
155
-
// const handleJetstream = async (subscription: JetstreamSubscription) => {
156
-
// for await (const event of subscription) {
157
-
// if (event.kind !== 'commit') continue;
158
-
// const commit = event.commit;
159
-
// if (commit.operation === 'delete') {
160
-
// continue;
161
-
// }
162
-
// const record = commit.record as AppBskyFeedPost.Main;
163
-
// addPosts(
164
-
// event.did,
165
-
// new Map([[`at://${event.did}/${commit.collection}/${commit.rkey}` as ResourceUri, record]])
166
-
// );
167
-
// }
168
-
// };
169
-
170
178
const loaderState = new LoaderState();
171
179
let scrollContainer = $state<HTMLDivElement>();
172
180
···
175
183
let showScrollToTop = $state(false);
176
184
177
185
const handleScroll = () => {
178
-
showScrollToTop = window.scrollY > 300;
186
+
if (currentView === 'timeline') showScrollToTop = window.scrollY > 300;
179
187
};
180
188
181
189
const scrollToTop = () => {
···
215
223
'app.bsky.feed.post:reply.parent.uri'
216
224
)
217
225
);
218
-
// jetstream.set(
219
-
// viewClient.streamJetstream(
220
-
// newAccounts.map((account) => account.did),
221
-
// 'app.bsky.feed.post'
222
-
// )
223
-
// );
224
226
});
225
227
notificationStream.subscribe((stream) => {
226
228
if (!stream) return;
227
229
stream.listen(handleNotification);
228
230
});
229
-
// jetstream.subscribe((stream) => {
230
-
// if (!stream) return;
231
-
// handleJetstream(stream);
232
-
// });
233
231
if ($accounts.length > 0) {
234
232
loaderState.status = 'LOADING';
235
233
if (loadData.client.ok && loadData.client.value) {
···
252
250
});
253
251
</script>
254
252
255
-
{#snippet appButton(onClick: () => void, icon: string, ariaLabel: string, iconHover?: string)}
253
+
{#snippet appButton(
254
+
onClick: () => void,
255
+
icon: string,
256
+
ariaLabel: string,
257
+
isActive: boolean,
258
+
iconHover?: string
259
+
)}
256
260
<button
257
261
onclick={onClick}
258
-
class="group rounded-sm bg-(--nucleus-accent)/15 p-2 text-(--nucleus-accent) transition-all hover:scale-110 hover:shadow-lg"
262
+
class="group rounded-sm p-2 transition-all hover:scale-110 hover:shadow-lg
263
+
{isActive
264
+
? 'bg-(--nucleus-accent)/25 text-(--nucleus-accent)'
265
+
: 'bg-(--nucleus-accent)/10 text-(--nucleus-accent) hover:bg-(--nucleus-accent)/15'}"
259
266
aria-label={ariaLabel}
260
267
>
261
268
<Icon class="group-hover:hidden" {icon} width={28} />
···
263
270
</button>
264
271
{/snippet}
265
272
266
-
<div class="mx-auto max-w-2xl">
267
-
<!-- thread list (page scrolls as a whole) -->
268
-
<div
269
-
id="app-thread-list"
270
-
class="mb-4 min-h-screen p-2 [scrollbar-color:var(--nucleus-accent)_transparent]"
271
-
bind:this={scrollContainer}
272
-
>
273
-
{#if $accounts.length > 0}
274
-
{@render renderThreads()}
275
-
{:else}
276
-
<div class="flex justify-center py-4">
277
-
<p class="text-xl opacity-80">
278
-
<span class="text-4xl">x_x</span> <br /> no accounts are logged in!
279
-
</p>
273
+
<div class="mx-auto flex min-h-dvh max-w-2xl flex-col">
274
+
<!-- Views Container -->
275
+
<div class="flex-1">
276
+
<!-- timeline -->
277
+
<div
278
+
id="app-thread-list"
279
+
class="min-h-full p-2 [scrollbar-color:var(--nucleus-accent)_transparent] {currentView ===
280
+
'timeline'
281
+
? `block ${animClass}`
282
+
: 'hidden'}"
283
+
bind:this={scrollContainer}
284
+
>
285
+
{#if $accounts.length > 0}
286
+
{@render renderThreads()}
287
+
{:else}
288
+
<div class="flex justify-center py-4">
289
+
<p class="text-xl opacity-80">
290
+
<span class="text-4xl">x_x</span> <br /> no accounts are logged in!
291
+
</p>
292
+
</div>
293
+
{/if}
294
+
</div>
295
+
296
+
<!-- other views -->
297
+
{#if currentView === 'settings'}
298
+
<div class={animClass}>
299
+
<SettingsView />
300
+
</div>
301
+
{:else if currentView === 'notifications'}
302
+
<div class={animClass}>
303
+
<NotificationsView />
280
304
</div>
281
305
{/if}
282
306
</div>
283
307
284
-
<!-- header -->
285
-
<div class="sticky bottom-0 z-10">
308
+
<!-- header / footer -->
309
+
<div id="app-footer" class="sticky bottom-0 z-10 mt-4">
286
310
{#if errors.length > 0}
287
311
<div class="relative m-3 mb-1 error-disclaimer">
288
312
<div class="flex items-center gap-2 text-red-500">
···
315
339
background: linear-gradient(to right, color-mix(in srgb, var(--nucleus-accent) 18%, var(--nucleus-bg)), color-mix(in srgb, var(--nucleus-accent2) 13%, var(--nucleus-bg)));
316
340
"
317
341
>
318
-
<!-- composer and error disclaimer (above thread list, not scrollable) -->
319
-
<div class="flex gap-2 px-2 pt-2 pb-1">
320
-
<AccountSelector
321
-
client={viewClient}
322
-
accounts={$accounts}
323
-
bind:selectedDid
324
-
onAccountSelected={handleAccountSelected}
325
-
onLogout={handleLogout}
326
-
/>
342
+
{#if currentView === 'timeline'}
343
+
<!-- composer and error disclaimer (above thread list, not scrollable) -->
344
+
<div class="flex gap-2 px-2 pt-2 pb-1">
345
+
<AccountSelector
346
+
client={viewClient}
347
+
accounts={$accounts}
348
+
bind:selectedDid
349
+
onAccountSelected={handleAccountSelected}
350
+
onLogout={handleLogout}
351
+
/>
327
352
328
-
{#if selectedClient}
329
-
<div class="flex-1">
330
-
<PostComposer
331
-
client={selectedClient}
332
-
onPostSent={(post) => posts.get(selectedDid!)?.set(post.uri, post)}
333
-
bind:_state={postComposerState}
334
-
/>
335
-
</div>
336
-
{:else}
337
-
<div
338
-
class="flex flex-1 items-center justify-center rounded-sm border-2 border-(--nucleus-accent)/20 bg-(--nucleus-accent)/4 px-4 py-2.5 backdrop-blur-sm"
339
-
>
340
-
<p class="text-sm opacity-80">select or add an account to post</p>
341
-
</div>
342
-
{/if}
353
+
{#if selectedClient}
354
+
<div class="flex-1">
355
+
<PostComposer
356
+
client={selectedClient}
357
+
onPostSent={(post) => posts.get(selectedDid!)?.set(post.uri, post)}
358
+
bind:_state={postComposerState}
359
+
/>
360
+
</div>
361
+
{:else}
362
+
<div
363
+
class="flex flex-1 items-center justify-center rounded-sm border-2 border-(--nucleus-accent)/20 bg-(--nucleus-accent)/4 px-4 py-2.5 backdrop-blur-sm"
364
+
>
365
+
<p class="text-sm opacity-80">select or add an account to post</p>
366
+
</div>
367
+
{/if}
343
368
344
-
{#if postComposerState.type === 'null' && showScrollToTop}
345
-
{@render appButton(scrollToTop, 'heroicons:arrow-up-16-solid', 'scroll to top')}
346
-
{/if}
347
-
</div>
369
+
{#if postComposerState.type === 'null' && showScrollToTop}
370
+
{@render appButton(
371
+
scrollToTop,
372
+
'heroicons:arrow-up-16-solid',
373
+
'scroll to top',
374
+
false
375
+
)}
376
+
{/if}
377
+
</div>
348
378
349
-
<div
350
-
class="mt-1 h-px w-full opacity-50"
351
-
style="background: linear-gradient(to right, var(--nucleus-accent), var(--nucleus-accent2));"
352
-
></div>
379
+
<div
380
+
class="mt-1 h-px w-full opacity-50"
381
+
style="background: linear-gradient(to right, var(--nucleus-accent), var(--nucleus-accent2));"
382
+
></div>
383
+
{/if}
353
384
354
385
<div class="flex items-center gap-1.5 px-2 py-1">
355
386
<div class="mb-2">
···
361
392
</div>
362
393
<div class="grow"></div>
363
394
{@render appButton(
364
-
() => (isNotificationsOpen = true),
395
+
() => switchView('timeline'),
396
+
'heroicons:home',
397
+
'timeline',
398
+
currentView === 'timeline',
399
+
'heroicons:home-solid'
400
+
)}
401
+
{@render appButton(
402
+
() => switchView('notifications'),
365
403
'heroicons:bell',
366
404
'notifications',
405
+
currentView === 'notifications',
367
406
'heroicons:bell-solid'
368
407
)}
369
408
{@render appButton(
370
-
() => (isSettingsOpen = true),
409
+
() => switchView('settings'),
371
410
'heroicons:cog-6-tooth',
372
411
'settings',
412
+
currentView === 'settings',
373
413
'heroicons:cog-6-tooth-solid'
374
414
)}
375
415
</div>
376
-
377
-
<!-- <hr
378
-
class="h-[4px] w-full rounded-full border-0"
379
-
style="background: linear-gradient(to right, var(--nucleus-accent), var(--nucleus-accent2));"
380
-
/> -->
381
416
</div>
382
417
</div>
383
418
</div>
384
419
</div>
385
-
386
-
<SettingsPopup bind:isOpen={isSettingsOpen} onClose={() => (isSettingsOpen = false)} />
387
-
<NotificationsPopup
388
-
bind:isOpen={isNotificationsOpen}
389
-
onClose={() => (isNotificationsOpen = false)}
390
-
/>
391
420
392
421
{#snippet replyPost(post: ThreadPost, reverse: boolean = reverseChronological)}
393
422
<span