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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -0
  3. package/bin/birdclaw.mjs +50 -11
  4. package/package.json +7 -6
  5. package/public/birdclaw-mark.png +0 -0
  6. package/scripts/browser-perf.mjs +399 -0
  7. package/scripts/build-docs-site.mjs +940 -0
  8. package/scripts/docs-site-assets.mjs +311 -0
  9. package/scripts/run-vitest.mjs +21 -0
  10. package/scripts/sanitize-node-options.mjs +23 -0
  11. package/scripts/start-test-server.mjs +29 -0
  12. package/src/cli.ts +46 -1
  13. package/src/components/AppNav.tsx +3 -3
  14. package/src/components/BrandMark.tsx +67 -0
  15. package/src/components/ConversationThread.tsx +11 -10
  16. package/src/components/DmWorkspace.tsx +24 -9
  17. package/src/components/FeedState.tsx +147 -0
  18. package/src/components/InboxCard.tsx +14 -2
  19. package/src/components/SavedTimelineView.tsx +64 -56
  20. package/src/components/SyncNowButton.tsx +105 -0
  21. package/src/components/ThemeSlider.tsx +10 -59
  22. package/src/components/TimelineCard.tsx +157 -61
  23. package/src/components/TimelineRouteFrame.tsx +156 -0
  24. package/src/components/TweetMediaGrid.tsx +120 -23
  25. package/src/components/TweetRichText.tsx +13 -2
  26. package/src/components/useTimelineRouteData.ts +137 -0
  27. package/src/lib/api-client.ts +229 -0
  28. package/src/lib/archive-finder.ts +24 -20
  29. package/src/lib/archive-import.ts +18 -3
  30. package/src/lib/conversation-surface.ts +174 -0
  31. package/src/lib/db.ts +2 -0
  32. package/src/lib/queries.ts +93 -28
  33. package/src/lib/ui.ts +11 -3
  34. package/src/lib/web-sync.ts +443 -0
  35. package/src/routeTree.gen.ts +21 -0
  36. package/src/routes/api/sync.tsx +59 -0
  37. package/src/routes/dms.tsx +100 -27
  38. package/src/routes/index.tsx +21 -127
  39. package/src/routes/links.tsx +50 -5
  40. package/src/routes/mentions.tsx +21 -127
  41. package/src/styles.css +74 -11
  42. package/vite.config.ts +8 -0
@@ -1,138 +1,32 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { Search } from "lucide-react";
3
- import { useEffect, useMemo, useState } from "react";
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
- const TABS: Array<{ value: ReplyFilter; label: string }> = [
33
- { value: "all", label: "All" },
34
- { value: "unreplied", label: "Unreplied" },
35
- { value: "replied", label: "Replied" },
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
- <header className={pageHeaderClass}>
91
- <div className={pageHeaderRowClass}>
92
- <div className="flex min-w-0 flex-col">
93
- <h1 className={pageTitleClass}>Mentions</h1>
94
- <p className={pageSubtitleClass}>{subtitle}</p>
95
- </div>
96
- </div>
97
- <div className="px-4 pb-3">
98
- <label className={searchFieldShellClass}>
99
- <Search className={searchFieldIconClass} strokeWidth={2} />
100
- <input
101
- className={searchFieldInputClass}
102
- onChange={(event) => setSearch(event.target.value)}
103
- placeholder="Search mentions"
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: #000000;
44
- --bg-elevated: #16181c;
45
- --bg-hover: #16181c;
46
- --bg-active: #1d1f23;
47
- --panel: #000000;
48
- --panel-strong: #16181c;
49
- --line: #2f3336;
50
- --line-strong: #536471;
51
- --ink: #e7e9ea;
52
- --ink-soft: #71767b;
53
- --ink-mute: #5b6166;
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;