1<script lang="ts">
2 import type { AtpClient } from '$lib/at/client';
3 import { ok, err, type Result } from '$lib/result';
4 import type { AppBskyFeedPost } from '@atcute/bluesky';
5 import type { ResourceUri } from '@atcute/lexicons';
6 import { generateColorForDid } from '$lib/accounts';
7
8 interface Props {
9 client: AtpClient;
10 onPostSent: (uri: ResourceUri, post: AppBskyFeedPost.Main) => void;
11 }
12
13 const { client, onPostSent }: Props = $props();
14
15 let color = $derived(
16 client.didDoc?.did ? generateColorForDid(client.didDoc?.did) : 'var(--nucleus-accent)'
17 );
18
19 const post = async (
20 text: string
21 ): Promise<Result<{ uri: ResourceUri; record: AppBskyFeedPost.Main }, string>> => {
22 const record: AppBskyFeedPost.Main = {
23 $type: 'app.bsky.feed.post',
24 text,
25 createdAt: new Date().toISOString()
26 };
27
28 const res = await client.atcute?.post('com.atproto.repo.createRecord', {
29 input: {
30 collection: 'app.bsky.feed.post',
31 repo: client.didDoc!.did,
32 record
33 }
34 });
35
36 if (!res) {
37 return err('failed to post: not logged in');
38 }
39
40 if (!res.ok) {
41 return err(`failed to post: ${res.data.error}: ${res.data.message ?? 'no details'}`);
42 }
43
44 return ok({
45 uri: res.data.uri,
46 record
47 });
48 };
49
50 let postText = $state('');
51 let info = $state('');
52 let isFocused = $state(false);
53 let textareaEl: HTMLTextAreaElement | undefined = $state();
54
55 const doPost = () => {
56 if (postText.length === 0 || postText.length > 300) return;
57
58 post(postText).then((res) => {
59 if (res.ok) {
60 onPostSent(res.value.uri, res.value.record);
61 postText = '';
62 info = 'posted!';
63 setTimeout(() => (info = ''), 1000 * 3);
64 } else {
65 info = res.error;
66 }
67 });
68 };
69
70 $effect(() => {
71 if (isFocused && textareaEl) textareaEl.focus();
72 });
73</script>
74
75<div class="relative min-h-16">
76 <!-- Spacer to maintain layout when focused -->
77 {#if isFocused}
78 <div class="min-h-16"></div>
79 {/if}
80
81 <div
82 class="flex max-w-full rounded-sm border-2 shadow-lg transition-all duration-300"
83 class:min-h-16={!isFocused}
84 class:items-center={!isFocused}
85 class:shadow-2xl={isFocused}
86 class:absolute={isFocused}
87 class:top-0={isFocused}
88 class:left-0={isFocused}
89 class:right-0={isFocused}
90 class:z-50={isFocused}
91 style="background: {isFocused
92 ? `color-mix(in srgb, var(--nucleus-bg) 80%, ${color} 20%)`
93 : `color-mix(in srgb, ${color} 9%, transparent)`};
94 border-color: color-mix(in srgb, {color} {isFocused ? '100' : '40'}%, transparent);"
95 >
96 <div class="w-full p-2" class:py-3={isFocused}>
97 {#if info.length > 0}
98 <div
99 class="rounded-sm px-3 py-1.5 text-center font-medium text-nowrap overflow-ellipsis"
100 style="background: color-mix(in srgb, {color} 13%, transparent); color: {color};"
101 >
102 {info}
103 </div>
104 {:else}
105 <div class="flex flex-col gap-2">
106 {#if isFocused}
107 <textarea
108 bind:this={textareaEl}
109 bind:value={postText}
110 onfocus={() => (isFocused = true)}
111 onblur={() => (isFocused = false)}
112 onkeydown={(event) => {
113 if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) doPost();
114 }}
115 placeholder="what's on your mind?"
116 rows="4"
117 class="[field-sizing:content] single-line-input resize-none bg-(--nucleus-bg)/40 focus:scale-100"
118 style="border-color: color-mix(in srgb, {color} 27%, transparent);"
119 ></textarea>
120 <div class="flex items-center gap-2">
121 <div class="grow"></div>
122 <span
123 class="text-sm font-medium"
124 style="color: color-mix(in srgb, {postText.length > 300
125 ? '#ef4444'
126 : 'var(--nucleus-fg)'} 53%, transparent);"
127 >
128 {postText.length} / 300
129 </span>
130 <button
131 onclick={doPost}
132 disabled={postText.length === 0 || postText.length > 300}
133 class="action-button border-none px-5 text-(--nucleus-fg)/94 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100"
134 style="background: color-mix(in srgb, {color} 87%, transparent);"
135 >
136 post
137 </button>
138 </div>
139 {:else}
140 <input
141 bind:value={postText}
142 onfocus={() => (isFocused = true)}
143 onkeydown={(event) => {
144 if (event.key === 'Enter') doPost();
145 }}
146 type="text"
147 placeholder="what's on your mind?"
148 class="single-line-input flex-1 bg-(--nucleus-bg)/40"
149 style="border-color: color-mix(in srgb, {color} 27%, transparent);"
150 />
151 {/if}
152 </div>
153 {/if}
154 </div>
155 </div>
156</div>