birdclaw 0.4.0 → 0.5.0

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 (64) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +108 -7
  3. package/package.json +24 -23
  4. package/playwright.config.ts +1 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/logo192.png +0 -0
  7. package/public/logo512.png +0 -0
  8. package/public/manifest.json +2 -2
  9. package/src/cli.ts +563 -18
  10. package/src/components/AppNav.tsx +66 -29
  11. package/src/components/AvatarChip.tsx +10 -5
  12. package/src/components/ConversationThread.tsx +125 -0
  13. package/src/components/DmWorkspace.tsx +96 -98
  14. package/src/components/EmbeddedTweetCard.tsx +20 -14
  15. package/src/components/InboxCard.tsx +92 -90
  16. package/src/components/LinkPreviewCard.tsx +270 -0
  17. package/src/components/ProfilePreview.tsx +8 -3
  18. package/src/components/SavedTimelineView.tsx +48 -38
  19. package/src/components/TimelineCard.tsx +228 -84
  20. package/src/components/TweetRichText.tsx +6 -2
  21. package/src/lib/archive-import.ts +1567 -65
  22. package/src/lib/authored-live.ts +1074 -0
  23. package/src/lib/backup.ts +318 -14
  24. package/src/lib/bird-actions.ts +1 -10
  25. package/src/lib/bird-command.ts +57 -0
  26. package/src/lib/bird.ts +89 -5
  27. package/src/lib/config.ts +1 -1
  28. package/src/lib/db.ts +282 -4
  29. package/src/lib/follow-graph.ts +1053 -0
  30. package/src/lib/link-index.ts +629 -0
  31. package/src/lib/link-insights.ts +834 -0
  32. package/src/lib/link-preview-metadata.ts +334 -0
  33. package/src/lib/media-fetch.ts +787 -0
  34. package/src/lib/media-includes.ts +165 -0
  35. package/src/lib/mention-threads-live.ts +535 -43
  36. package/src/lib/mentions-live.ts +623 -19
  37. package/src/lib/moderation-target.ts +6 -0
  38. package/src/lib/profile-hydration.ts +1 -1
  39. package/src/lib/profile-resolver.ts +115 -1
  40. package/src/lib/queries.ts +233 -7
  41. package/src/lib/seed.ts +127 -8
  42. package/src/lib/timeline-collections-live.ts +145 -22
  43. package/src/lib/timeline-live.ts +10 -15
  44. package/src/lib/tweet-account-edges.ts +9 -2
  45. package/src/lib/tweet-render.ts +97 -0
  46. package/src/lib/types.ts +219 -2
  47. package/src/lib/ui.ts +375 -147
  48. package/src/lib/url-expansion-store.ts +110 -0
  49. package/src/lib/url-expansion.ts +33 -8
  50. package/src/lib/x-profile.ts +7 -2
  51. package/src/lib/xurl.ts +296 -12
  52. package/src/routeTree.gen.ts +105 -0
  53. package/src/routes/__root.tsx +7 -3
  54. package/src/routes/api/conversation.tsx +34 -0
  55. package/src/routes/api/link-insights.tsx +79 -0
  56. package/src/routes/api/link-preview.tsx +43 -0
  57. package/src/routes/api/profile-hydrate.tsx +51 -0
  58. package/src/routes/blocks.tsx +111 -86
  59. package/src/routes/dms.tsx +90 -78
  60. package/src/routes/inbox.tsx +98 -86
  61. package/src/routes/index.tsx +63 -50
  62. package/src/routes/links.tsx +883 -0
  63. package/src/routes/mentions.tsx +63 -50
  64. package/src/styles.css +106 -43
@@ -1,4 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
+ import { Search } from "lucide-react";
2
3
  import { useEffect, useMemo, useState } from "react";
3
4
  import { TimelineCard } from "#/components/TimelineCard";
4
5
  import type {
@@ -9,25 +10,31 @@ import type {
9
10
  } from "#/lib/types";
10
11
  import {
11
12
  cx,
12
- eyebrowClass,
13
- feedPageClass,
14
- heroControlsClass,
15
- heroCopyClass,
16
- heroShellClass,
17
- heroTitleClass,
18
- pageWrapClass,
19
- segmentActiveClass,
20
- segmentClass,
21
- segmentedClass,
22
- textFieldClass,
23
- textFieldWideClass,
24
- timelineLaneClass,
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,
25
26
  } from "#/lib/ui";
26
27
 
27
28
  export const Route = createFileRoute("/mentions")({
28
29
  component: MentionsRoute,
29
30
  });
30
31
 
32
+ const TABS: Array<{ value: ReplyFilter; label: string }> = [
33
+ { value: "all", label: "All" },
34
+ { value: "unreplied", label: "Unreplied" },
35
+ { value: "replied", label: "Replied" },
36
+ ];
37
+
31
38
  function MentionsRoute() {
32
39
  const [meta, setMeta] = useState<QueryEnvelope | null>(null);
33
40
  const [items, setItems] = useState<TimelineItem[]>([]);
@@ -57,7 +64,7 @@ function MentionsRoute() {
57
64
 
58
65
  const subtitle = useMemo(() => {
59
66
  if (!meta) return "Loading mentions...";
60
- return `${meta.stats.mentions} mention/reply items in local store`;
67
+ return `${String(meta.stats.mentions)} mention/reply items in local store`;
61
68
  }, [meta]);
62
69
 
63
70
  async function replyToTweet(tweetId: string) {
@@ -79,47 +86,53 @@ function MentionsRoute() {
79
86
  }
80
87
 
81
88
  return (
82
- <main className={pageWrapClass}>
83
- <div className={feedPageClass}>
84
- <section className={heroShellClass}>
85
- <div>
86
- <p className={eyebrowClass}>mentions and replies</p>
87
- <h2 className={heroTitleClass}>
88
- Keep the actionable queue small and visible.
89
- </h2>
90
- <p className={heroCopyClass}>{subtitle}</p>
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>
91
95
  </div>
92
- <div className={heroControlsClass}>
96
+ </div>
97
+ <div className="px-4 pb-3">
98
+ <label className={searchFieldShellClass}>
99
+ <Search className={searchFieldIconClass} strokeWidth={2} />
93
100
  <input
94
- className={cx(textFieldClass, textFieldWideClass)}
101
+ className={searchFieldInputClass}
95
102
  onChange={(event) => setSearch(event.target.value)}
96
103
  placeholder="Search mentions"
97
104
  value={search}
98
105
  />
99
- <div className={segmentedClass}>
100
- {(["all", "replied", "unreplied"] as const).map((value) => (
101
- <button
102
- key={value}
103
- className={cx(
104
- segmentClass,
105
- value === replyFilter && segmentActiveClass,
106
- )}
107
- onClick={() => setReplyFilter(value)}
108
- type="button"
109
- >
110
- {value}
111
- </button>
112
- ))}
113
- </div>
114
- </div>
115
- </section>
116
-
117
- <section className={timelineLaneClass}>
118
- {items.map((item) => (
119
- <TimelineCard key={item.id} item={item} onReply={replyToTweet} />
120
- ))}
121
- </section>
122
- </div>
123
- </main>
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
+ </>
124
137
  );
125
138
  }
package/src/styles.css CHANGED
@@ -1,42 +1,68 @@
1
- @import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Instrument+Sans:wght@400;500;600;700&display=swap");
2
1
  @import "tailwindcss";
3
2
 
4
3
  @theme inline {
5
- --font-sans: "Instrument Sans", sans-serif;
6
- --font-display: "Fraunces", serif;
4
+ --font-sans:
5
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
6
+ Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;
7
+ --font-display: var(--font-sans);
8
+ --font-mono:
9
+ ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
7
10
  }
8
11
 
9
12
  :root {
10
- --bg: #f5f0e8;
11
- --panel: rgb(255 252 247 / 92%);
12
- --panel-strong: rgb(255 255 255 / 82%);
13
- --line: rgb(32 24 17 / 12%);
14
- --line-strong: rgb(32 24 17 / 24%);
15
- --ink: #19130e;
16
- --ink-soft: rgb(25 19 14 / 68%);
17
- --accent: #1d6f53;
18
- --accent-soft: rgb(29 111 83 / 12%);
19
- --alert: #a34a2d;
20
- --alert-soft: rgb(163 74 45 / 14%);
21
- --shadow: rgb(32 24 17 / 8%);
13
+ --bg: #ffffff;
14
+ --bg-elevated: #ffffff;
15
+ --bg-hover: #f7f9f9;
16
+ --bg-active: #eff3f4;
17
+ --panel: #ffffff;
18
+ --panel-strong: #ffffff;
19
+ --line: #eff3f4;
20
+ --line-strong: #cfd9de;
21
+ --ink: #0f1419;
22
+ --ink-soft: #536471;
23
+ --ink-mute: #8b98a5;
24
+ --accent: #1d9bf0;
25
+ --accent-hover: #1a8cd8;
26
+ --accent-press: #177cc1;
27
+ --accent-soft: rgb(29 155 240 / 10%);
28
+ --accent-text: #ffffff;
29
+ --alert: #f4212e;
30
+ --alert-soft: rgb(244 33 46 / 10%);
31
+ --like: #f91880;
32
+ --like-soft: rgb(249 24 128 / 10%);
33
+ --accent-on-ink: #1d9bf0;
34
+ --ring: rgb(29 155 240 / 35%);
35
+ --shadow: rgb(15 20 25 / 6%);
36
+ --shadow-strong: rgb(15 20 25 / 12%);
22
37
  --theme-switch-x: 50%;
23
38
  --theme-switch-y: 50%;
24
39
  color-scheme: light;
25
40
  }
26
41
 
27
42
  [data-theme="dark"] {
28
- --bg: #151311;
29
- --panel: rgb(29 24 19 / 88%);
30
- --panel-strong: rgb(39 32 27 / 94%);
31
- --line: rgb(245 232 214 / 10%);
32
- --line-strong: rgb(245 232 214 / 18%);
33
- --ink: #f4ede3;
34
- --ink-soft: rgb(244 237 227 / 68%);
35
- --accent: #68c4a0;
36
- --accent-soft: rgb(104 196 160 / 14%);
37
- --alert: #d98a69;
38
- --alert-soft: rgb(217 138 105 / 16%);
39
- --shadow: rgb(0 0 0 / 34%);
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;
54
+ --accent: #1d9bf0;
55
+ --accent-hover: #1a8cd8;
56
+ --accent-press: #177cc1;
57
+ --accent-soft: rgb(29 155 240 / 18%);
58
+ --accent-text: #ffffff;
59
+ --alert: #f4212e;
60
+ --alert-soft: rgb(244 33 46 / 18%);
61
+ --like: #f91880;
62
+ --like-soft: rgb(249 24 128 / 18%);
63
+ --ring: rgb(29 155 240 / 45%);
64
+ --shadow: rgb(0 0 0 / 60%);
65
+ --shadow-strong: rgb(0 0 0 / 80%);
40
66
  color-scheme: dark;
41
67
  }
42
68
 
@@ -45,23 +71,60 @@ body {
45
71
  min-height: 100%;
46
72
  }
47
73
 
74
+ html {
75
+ scrollbar-gutter: stable;
76
+ }
77
+
48
78
  body {
49
- background:
50
- radial-gradient(
51
- circle at top left,
52
- color-mix(in srgb, var(--accent) 16%, transparent),
53
- transparent 28%
54
- ),
55
- radial-gradient(
56
- circle at right 18%,
57
- color-mix(in srgb, var(--alert) 12%, transparent),
58
- transparent 24%
59
- ),
60
- linear-gradient(
61
- 180deg,
62
- color-mix(in srgb, var(--bg) 82%, white) 0%,
63
- var(--bg) 100%
64
- );
79
+ background: var(--bg);
80
+ font-feature-settings: "ss01" on, "cv11" on;
81
+ }
82
+
83
+ ::selection {
84
+ background: color-mix(in srgb, var(--accent) 28%, transparent);
85
+ color: var(--ink);
86
+ }
87
+
88
+ * {
89
+ -webkit-tap-highlight-color: transparent;
90
+ }
91
+
92
+ *:focus-visible {
93
+ outline: 2px solid var(--ring);
94
+ outline-offset: 2px;
95
+ border-radius: 4px;
96
+ }
97
+
98
+ button,
99
+ a {
100
+ -webkit-user-select: none;
101
+ user-select: none;
102
+ }
103
+
104
+ input,
105
+ textarea {
106
+ -webkit-user-select: text;
107
+ user-select: text;
108
+ }
109
+
110
+ /* Subtle thin scrollbar for app-owned internal scroll panes. */
111
+ .custom-scrollbar::-webkit-scrollbar {
112
+ width: 10px;
113
+ height: 10px;
114
+ }
115
+
116
+ .custom-scrollbar::-webkit-scrollbar-track {
117
+ background: transparent;
118
+ }
119
+
120
+ .custom-scrollbar::-webkit-scrollbar-thumb {
121
+ background: color-mix(in srgb, var(--ink-soft) 30%, transparent);
122
+ border-radius: 999px;
123
+ border: 2px solid var(--bg);
124
+ }
125
+
126
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
127
+ background: color-mix(in srgb, var(--ink-soft) 55%, transparent);
65
128
  }
66
129
 
67
130
  @keyframes theme-circle-transition {