birdclaw 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +5 -0
- package/bin/birdclaw.mjs +50 -11
- package/package.json +7 -6
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +399 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +29 -0
- package/src/cli.ts +46 -1
- package/src/components/AppNav.tsx +3 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +24 -9
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +105 -0
- package/src/components/ThemeSlider.tsx +10 -59
- package/src/components/TimelineCard.tsx +157 -61
- package/src/components/TimelineRouteFrame.tsx +156 -0
- package/src/components/TweetMediaGrid.tsx +120 -23
- package/src/components/TweetRichText.tsx +13 -2
- package/src/components/useTimelineRouteData.ts +137 -0
- package/src/lib/api-client.ts +229 -0
- package/src/lib/archive-finder.ts +24 -20
- package/src/lib/archive-import.ts +18 -3
- package/src/lib/conversation-surface.ts +174 -0
- package/src/lib/db.ts +2 -0
- package/src/lib/queries.ts +93 -28
- package/src/lib/ui.ts +11 -3
- package/src/lib/web-sync.ts +443 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/api/sync.tsx +59 -0
- package/src/routes/dms.tsx +100 -27
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
package/src/routes/mentions.tsx
CHANGED
|
@@ -1,138 +1,32 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { TimelineCard } from "#/components/TimelineCard";
|
|
5
|
-
import type {
|
|
6
|
-
QueryEnvelope,
|
|
7
|
-
QueryResponse,
|
|
8
|
-
ReplyFilter,
|
|
9
|
-
TimelineItem,
|
|
10
|
-
} from "#/lib/types";
|
|
11
|
-
import {
|
|
12
|
-
cx,
|
|
13
|
-
emptyStateClass,
|
|
14
|
-
feedClass,
|
|
15
|
-
pageHeaderClass,
|
|
16
|
-
pageHeaderRowClass,
|
|
17
|
-
pageSubtitleClass,
|
|
18
|
-
pageTitleClass,
|
|
19
|
-
searchFieldIconClass,
|
|
20
|
-
searchFieldInputClass,
|
|
21
|
-
searchFieldShellClass,
|
|
22
|
-
tabButtonActiveClass,
|
|
23
|
-
tabButtonClass,
|
|
24
|
-
tabButtonIndicatorClass,
|
|
25
|
-
tabStripClass,
|
|
26
|
-
} from "#/lib/ui";
|
|
2
|
+
import { TimelineRouteFrame } from "#/components/TimelineRouteFrame";
|
|
3
|
+
import type { QueryEnvelope } from "#/lib/types";
|
|
27
4
|
|
|
28
5
|
export const Route = createFileRoute("/mentions")({
|
|
29
6
|
component: MentionsRoute,
|
|
30
7
|
});
|
|
31
8
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
];
|
|
9
|
+
function mentionsSubtitle(meta: QueryEnvelope | null) {
|
|
10
|
+
if (!meta) return "Loading mentions...";
|
|
11
|
+
return `${String(meta.stats.mentions)} mention/reply items in local store`;
|
|
12
|
+
}
|
|
37
13
|
|
|
38
14
|
function MentionsRoute() {
|
|
39
|
-
const [meta, setMeta] = useState<QueryEnvelope | null>(null);
|
|
40
|
-
const [items, setItems] = useState<TimelineItem[]>([]);
|
|
41
|
-
const [replyFilter, setReplyFilter] = useState<ReplyFilter>("unreplied");
|
|
42
|
-
const [search, setSearch] = useState("");
|
|
43
|
-
const [refreshTick, setRefreshTick] = useState(0);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
fetch("/api/status")
|
|
47
|
-
.then((response) => response.json())
|
|
48
|
-
.then((data: QueryEnvelope) => setMeta(data));
|
|
49
|
-
}, []);
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const url = new URL("/api/query", window.location.origin);
|
|
53
|
-
url.searchParams.set("resource", "mentions");
|
|
54
|
-
url.searchParams.set("replyFilter", replyFilter);
|
|
55
|
-
url.searchParams.set("refresh", String(refreshTick));
|
|
56
|
-
if (search.trim()) {
|
|
57
|
-
url.searchParams.set("search", search.trim());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
fetch(url)
|
|
61
|
-
.then((response) => response.json())
|
|
62
|
-
.then((data: QueryResponse) => setItems(data.items as TimelineItem[]));
|
|
63
|
-
}, [refreshTick, replyFilter, search]);
|
|
64
|
-
|
|
65
|
-
const subtitle = useMemo(() => {
|
|
66
|
-
if (!meta) return "Loading mentions...";
|
|
67
|
-
return `${String(meta.stats.mentions)} mention/reply items in local store`;
|
|
68
|
-
}, [meta]);
|
|
69
|
-
|
|
70
|
-
async function replyToTweet(tweetId: string) {
|
|
71
|
-
const text = window.prompt("Reply text");
|
|
72
|
-
if (!text?.trim()) return;
|
|
73
|
-
|
|
74
|
-
await fetch("/api/action", {
|
|
75
|
-
method: "POST",
|
|
76
|
-
headers: { "content-type": "application/json" },
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
kind: "replyTweet",
|
|
79
|
-
accountId: "acct_primary",
|
|
80
|
-
tweetId,
|
|
81
|
-
text,
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
setRefreshTick((value) => value + 1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
15
|
return (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
value={search}
|
|
105
|
-
/>
|
|
106
|
-
</label>
|
|
107
|
-
</div>
|
|
108
|
-
<div className={tabStripClass}>
|
|
109
|
-
{TABS.map((tab) => {
|
|
110
|
-
const active = replyFilter === tab.value;
|
|
111
|
-
return (
|
|
112
|
-
<button
|
|
113
|
-
key={tab.value}
|
|
114
|
-
type="button"
|
|
115
|
-
aria-pressed={active}
|
|
116
|
-
className={cx(tabButtonClass, active && tabButtonActiveClass)}
|
|
117
|
-
onClick={() => setReplyFilter(tab.value)}
|
|
118
|
-
>
|
|
119
|
-
<span className="relative inline-flex flex-col items-center justify-center py-1">
|
|
120
|
-
{tab.value}
|
|
121
|
-
{active ? <span className={tabButtonIndicatorClass} /> : null}
|
|
122
|
-
</span>
|
|
123
|
-
</button>
|
|
124
|
-
);
|
|
125
|
-
})}
|
|
126
|
-
</div>
|
|
127
|
-
</header>
|
|
128
|
-
<section className={feedClass}>
|
|
129
|
-
{items.length === 0 ? (
|
|
130
|
-
<div className={emptyStateClass}>No mentions in view.</div>
|
|
131
|
-
) : null}
|
|
132
|
-
{items.map((item) => (
|
|
133
|
-
<TimelineCard key={item.id} item={item} onReply={replyToTweet} />
|
|
134
|
-
))}
|
|
135
|
-
</section>
|
|
136
|
-
</>
|
|
16
|
+
<TimelineRouteFrame
|
|
17
|
+
emptyDetail="Try All, search less narrowly, or sync mentions."
|
|
18
|
+
emptyLabel="No mentions in this view"
|
|
19
|
+
errorFallback="Mentions unavailable"
|
|
20
|
+
errorTitle="Could not load mentions"
|
|
21
|
+
initialReplyFilter="unreplied"
|
|
22
|
+
loadingDetail="Checking local mentions and reply context"
|
|
23
|
+
loadingLabel="Loading mentions"
|
|
24
|
+
resource="mentions"
|
|
25
|
+
searchPlaceholder="Search mentions"
|
|
26
|
+
subtitle={mentionsSubtitle}
|
|
27
|
+
syncKind="mentions"
|
|
28
|
+
syncLabel="Sync mentions"
|
|
29
|
+
title="Mentions"
|
|
30
|
+
/>
|
|
137
31
|
);
|
|
138
32
|
}
|
package/src/styles.css
CHANGED
|
@@ -34,23 +34,24 @@
|
|
|
34
34
|
--ring: rgb(29 155 240 / 35%);
|
|
35
35
|
--shadow: rgb(15 20 25 / 6%);
|
|
36
36
|
--shadow-strong: rgb(15 20 25 / 12%);
|
|
37
|
+
--brand-shadow: rgb(29 155 240 / 22%);
|
|
37
38
|
--theme-switch-x: 50%;
|
|
38
39
|
--theme-switch-y: 50%;
|
|
39
40
|
color-scheme: light;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
[data-theme="dark"] {
|
|
43
|
-
--bg: #
|
|
44
|
-
--bg-elevated: #
|
|
45
|
-
--bg-hover: #
|
|
46
|
-
--bg-active: #
|
|
47
|
-
--panel: #
|
|
48
|
-
--panel-strong: #
|
|
49
|
-
--line: #
|
|
50
|
-
--line-strong: #
|
|
51
|
-
--ink: #
|
|
52
|
-
--ink-soft: #
|
|
53
|
-
--ink-mute: #
|
|
44
|
+
--bg: #0b0f14;
|
|
45
|
+
--bg-elevated: #10151b;
|
|
46
|
+
--bg-hover: #141a21;
|
|
47
|
+
--bg-active: #18212a;
|
|
48
|
+
--panel: #0b0f14;
|
|
49
|
+
--panel-strong: #121820;
|
|
50
|
+
--line: #26323d;
|
|
51
|
+
--line-strong: #44515d;
|
|
52
|
+
--ink: #d8dee5;
|
|
53
|
+
--ink-soft: #8b98a5;
|
|
54
|
+
--ink-mute: #687480;
|
|
54
55
|
--accent: #1d9bf0;
|
|
55
56
|
--accent-hover: #1a8cd8;
|
|
56
57
|
--accent-press: #177cc1;
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
--ring: rgb(29 155 240 / 45%);
|
|
64
65
|
--shadow: rgb(0 0 0 / 60%);
|
|
65
66
|
--shadow-strong: rgb(0 0 0 / 80%);
|
|
67
|
+
--brand-shadow: rgb(29 155 240 / 36%);
|
|
66
68
|
color-scheme: dark;
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -141,6 +143,62 @@ textarea {
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
@keyframes birdclaw-load-sway {
|
|
147
|
+
0%,
|
|
148
|
+
100% {
|
|
149
|
+
transform: translateY(0) rotate(-2deg) scale(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
40% {
|
|
153
|
+
transform: translateY(-5px) rotate(3deg) scale(1.04);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
70% {
|
|
157
|
+
transform: translateY(1px) rotate(-1deg) scale(0.99);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@keyframes birdclaw-load-glow {
|
|
162
|
+
0%,
|
|
163
|
+
100% {
|
|
164
|
+
opacity: 0.24;
|
|
165
|
+
transform: scale(0.82);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
50% {
|
|
169
|
+
opacity: 0.46;
|
|
170
|
+
transform: scale(1.08);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.birdclaw-state {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
min-height: 168px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.birdclaw-mark::before {
|
|
183
|
+
content: "";
|
|
184
|
+
position: absolute;
|
|
185
|
+
inset: 16%;
|
|
186
|
+
z-index: -1;
|
|
187
|
+
border-radius: 999px;
|
|
188
|
+
background: var(--accent-soft);
|
|
189
|
+
filter: blur(14px);
|
|
190
|
+
opacity: 0.34;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.birdclaw-mark-animated {
|
|
194
|
+
animation: birdclaw-load-sway 1.7s ease-in-out infinite;
|
|
195
|
+
transform-origin: 50% 56%;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.birdclaw-mark-animated::before {
|
|
199
|
+
animation: birdclaw-load-glow 1.7s ease-in-out infinite;
|
|
200
|
+
}
|
|
201
|
+
|
|
144
202
|
html.theme-transition {
|
|
145
203
|
view-transition-name: theme;
|
|
146
204
|
}
|
|
@@ -169,4 +227,9 @@ html.theme-transition::view-transition-new(theme) {
|
|
|
169
227
|
html.theme-transition::view-transition-new(theme) {
|
|
170
228
|
animation: none !important;
|
|
171
229
|
}
|
|
230
|
+
|
|
231
|
+
.birdclaw-mark-animated,
|
|
232
|
+
.birdclaw-mark-animated::before {
|
|
233
|
+
animation: none !important;
|
|
234
|
+
}
|
|
172
235
|
}
|
package/vite.config.ts
CHANGED
|
@@ -4,6 +4,11 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
|
4
4
|
import viteReact from "@vitejs/plugin-react";
|
|
5
5
|
import { defineConfig } from "vite";
|
|
6
6
|
|
|
7
|
+
const extraAllowedHosts =
|
|
8
|
+
process.env.BIRDCLAW_ALLOWED_HOSTS?.split(",")
|
|
9
|
+
.map((host) => host.trim())
|
|
10
|
+
.filter(Boolean) ?? [];
|
|
11
|
+
|
|
7
12
|
const config = defineConfig({
|
|
8
13
|
plugins: [
|
|
9
14
|
devtools(),
|
|
@@ -18,6 +23,9 @@ const config = defineConfig({
|
|
|
18
23
|
resolve: {
|
|
19
24
|
tsconfigPaths: true,
|
|
20
25
|
},
|
|
26
|
+
server: {
|
|
27
|
+
allowedHosts: ["clawmac.sheep-coho.ts.net", ...extraAllowedHosts],
|
|
28
|
+
},
|
|
21
29
|
});
|
|
22
30
|
|
|
23
31
|
export default config;
|