birdclaw 0.1.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 (74) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -0
  4. package/bin/birdclaw.mjs +28 -0
  5. package/package.json +100 -0
  6. package/playwright.config.ts +31 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/logo192.png +0 -0
  9. package/public/logo512.png +0 -0
  10. package/public/manifest.json +25 -0
  11. package/public/robots.txt +3 -0
  12. package/src/cli-moderation.ts +210 -0
  13. package/src/cli.ts +450 -0
  14. package/src/components/AppNav.tsx +49 -0
  15. package/src/components/AvatarChip.tsx +47 -0
  16. package/src/components/DmWorkspace.tsx +246 -0
  17. package/src/components/EmbeddedTweetCard.tsx +44 -0
  18. package/src/components/InboxCard.tsx +136 -0
  19. package/src/components/ProfilePreview.tsx +55 -0
  20. package/src/components/ThemeSlider.tsx +124 -0
  21. package/src/components/TimelineCard.tsx +118 -0
  22. package/src/components/TweetMediaGrid.tsx +39 -0
  23. package/src/components/TweetRichText.tsx +85 -0
  24. package/src/lib/actions-transport.ts +173 -0
  25. package/src/lib/archive-finder.ts +128 -0
  26. package/src/lib/archive-import.ts +736 -0
  27. package/src/lib/avatar-cache.ts +184 -0
  28. package/src/lib/bird-actions.ts +200 -0
  29. package/src/lib/bird.ts +183 -0
  30. package/src/lib/blocklist.ts +119 -0
  31. package/src/lib/blocks-write.ts +118 -0
  32. package/src/lib/blocks.ts +326 -0
  33. package/src/lib/config.ts +152 -0
  34. package/src/lib/db.ts +326 -0
  35. package/src/lib/dms-live.ts +279 -0
  36. package/src/lib/inbox.ts +210 -0
  37. package/src/lib/mentions-export.ts +147 -0
  38. package/src/lib/mentions-live.ts +475 -0
  39. package/src/lib/moderation-target.ts +171 -0
  40. package/src/lib/moderation-write.ts +72 -0
  41. package/src/lib/mutes-write.ts +118 -0
  42. package/src/lib/mutes.ts +77 -0
  43. package/src/lib/openai.ts +86 -0
  44. package/src/lib/present.ts +20 -0
  45. package/src/lib/profile-hydration.ts +144 -0
  46. package/src/lib/profile-replies.ts +60 -0
  47. package/src/lib/queries.ts +725 -0
  48. package/src/lib/seed.ts +486 -0
  49. package/src/lib/sync-cache.ts +65 -0
  50. package/src/lib/theme-transition.ts +117 -0
  51. package/src/lib/theme.tsx +157 -0
  52. package/src/lib/tweet-render.ts +115 -0
  53. package/src/lib/types.ts +316 -0
  54. package/src/lib/ui.ts +270 -0
  55. package/src/lib/x-profile.ts +203 -0
  56. package/src/lib/x-web.ts +168 -0
  57. package/src/lib/xurl.ts +492 -0
  58. package/src/routeTree.gen.ts +282 -0
  59. package/src/router.tsx +20 -0
  60. package/src/routes/__root.tsx +64 -0
  61. package/src/routes/api/action.tsx +88 -0
  62. package/src/routes/api/avatar.tsx +41 -0
  63. package/src/routes/api/blocks.tsx +32 -0
  64. package/src/routes/api/inbox.tsx +35 -0
  65. package/src/routes/api/query.tsx +72 -0
  66. package/src/routes/api/status.tsx +15 -0
  67. package/src/routes/blocks.tsx +352 -0
  68. package/src/routes/dms.tsx +262 -0
  69. package/src/routes/inbox.tsx +201 -0
  70. package/src/routes/index.tsx +125 -0
  71. package/src/routes/mentions.tsx +125 -0
  72. package/src/styles.css +109 -0
  73. package/tsconfig.json +30 -0
  74. package/vite.config.ts +23 -0
@@ -0,0 +1,201 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { InboxCard } from "#/components/InboxCard";
4
+ import type {
5
+ InboxItem,
6
+ InboxKind,
7
+ InboxResponse,
8
+ QueryEnvelope,
9
+ } from "#/lib/types";
10
+ import {
11
+ actionButtonClass,
12
+ cx,
13
+ eyebrowClass,
14
+ feedPageClass,
15
+ heroControlsClass,
16
+ heroCopyClass,
17
+ heroShellClass,
18
+ heroTitleClass,
19
+ inboxLaneClass,
20
+ navLinkActiveClass,
21
+ navLinkClass,
22
+ pageWrapClass,
23
+ segmentActiveClass,
24
+ segmentClass,
25
+ segmentedClass,
26
+ textFieldClass,
27
+ textFieldShortClass,
28
+ timestampClass,
29
+ } from "#/lib/ui";
30
+
31
+ export const Route = createFileRoute("/inbox")({
32
+ component: InboxRoute,
33
+ });
34
+
35
+ function InboxRoute() {
36
+ const [meta, setMeta] = useState<QueryEnvelope | null>(null);
37
+ const [items, setItems] = useState<InboxItem[]>([]);
38
+ const [kind, setKind] = useState<InboxKind>("mixed");
39
+ const [minScore, setMinScore] = useState("40");
40
+ const [hideLowSignal, setHideLowSignal] = useState(true);
41
+ const [refreshTick, setRefreshTick] = useState(0);
42
+ const [isScoring, setIsScoring] = useState(false);
43
+ const [activeReplyId, setActiveReplyId] = useState<string | null>(null);
44
+ const [replyDraft, setReplyDraft] = useState("");
45
+ const [isSendingReply, setIsSendingReply] = useState(false);
46
+ const [stats, setStats] = useState<InboxResponse["stats"] | null>(null);
47
+
48
+ useEffect(() => {
49
+ fetch("/api/status")
50
+ .then((response) => response.json())
51
+ .then((data: QueryEnvelope) => setMeta(data));
52
+ }, []);
53
+
54
+ useEffect(() => {
55
+ const url = new URL("/api/inbox", window.location.origin);
56
+ url.searchParams.set("kind", kind);
57
+ url.searchParams.set("minScore", minScore);
58
+ url.searchParams.set("refresh", String(refreshTick));
59
+ if (hideLowSignal) {
60
+ url.searchParams.set("hideLowSignal", "1");
61
+ }
62
+
63
+ fetch(url)
64
+ .then((response) => response.json())
65
+ .then((data: InboxResponse) => {
66
+ setItems(data.items);
67
+ setStats(data.stats);
68
+ });
69
+ }, [hideLowSignal, kind, minScore, refreshTick]);
70
+
71
+ const subtitle = useMemo(() => {
72
+ if (!meta || !stats) return "Ranking unreplied mentions and DMs...";
73
+ return `${stats.total} items in queue · ${stats.openai} OpenAI scored · ${meta.transport.statusText}`;
74
+ }, [meta, stats]);
75
+
76
+ async function scoreNow() {
77
+ setIsScoring(true);
78
+ try {
79
+ await fetch("/api/action", {
80
+ method: "POST",
81
+ headers: { "content-type": "application/json" },
82
+ body: JSON.stringify({
83
+ kind: "scoreInbox",
84
+ scoreKind: kind,
85
+ limit: 8,
86
+ }),
87
+ });
88
+ setRefreshTick((value) => value + 1);
89
+ } finally {
90
+ setIsScoring(false);
91
+ }
92
+ }
93
+
94
+ async function sendReply(item: InboxItem) {
95
+ if (!replyDraft.trim()) return;
96
+ setIsSendingReply(true);
97
+ try {
98
+ await fetch("/api/action", {
99
+ method: "POST",
100
+ headers: { "content-type": "application/json" },
101
+ body: JSON.stringify(
102
+ item.entityKind === "dm"
103
+ ? {
104
+ kind: "replyDm",
105
+ conversationId: item.entityId,
106
+ text: replyDraft,
107
+ }
108
+ : {
109
+ kind: "replyTweet",
110
+ accountId: item.accountId,
111
+ tweetId: item.entityId,
112
+ text: replyDraft,
113
+ },
114
+ ),
115
+ });
116
+ setReplyDraft("");
117
+ setActiveReplyId(null);
118
+ setRefreshTick((value) => value + 1);
119
+ } finally {
120
+ setIsSendingReply(false);
121
+ }
122
+ }
123
+
124
+ return (
125
+ <main className={pageWrapClass}>
126
+ <div className={feedPageClass}>
127
+ <section className={heroShellClass}>
128
+ <div>
129
+ <p className={eyebrowClass}>inbox</p>
130
+ <h2 className={heroTitleClass}>AI triage for mentions and DMs.</h2>
131
+ <p className={heroCopyClass}>{subtitle}</p>
132
+ </div>
133
+ <div className={heroControlsClass}>
134
+ <div className={segmentedClass}>
135
+ {(["mixed", "mentions", "dms"] as const).map((value) => (
136
+ <button
137
+ key={value}
138
+ className={cx(
139
+ segmentClass,
140
+ value === kind && segmentActiveClass,
141
+ )}
142
+ onClick={() => setKind(value)}
143
+ type="button"
144
+ >
145
+ {value}
146
+ </button>
147
+ ))}
148
+ </div>
149
+ <input
150
+ className={cx(textFieldClass, textFieldShortClass)}
151
+ inputMode="numeric"
152
+ onChange={(event) => setMinScore(event.target.value)}
153
+ placeholder="Min AI score"
154
+ value={minScore}
155
+ />
156
+ <button
157
+ className={cx(navLinkClass, hideLowSignal && navLinkActiveClass)}
158
+ onClick={() => setHideLowSignal((value) => !value)}
159
+ type="button"
160
+ >
161
+ {hideLowSignal ? "Hide low-signal" : "Show all"}
162
+ </button>
163
+ <button
164
+ className={actionButtonClass}
165
+ disabled={isScoring}
166
+ onClick={() => void scoreNow()}
167
+ type="button"
168
+ >
169
+ {isScoring ? "Scoring..." : "Score with OpenAI"}
170
+ </button>
171
+ </div>
172
+ </section>
173
+
174
+ <section className={inboxLaneClass}>
175
+ {items.map((item) => (
176
+ <InboxCard
177
+ key={item.id}
178
+ isReplying={activeReplyId === item.id}
179
+ item={item}
180
+ onReplyChange={setReplyDraft}
181
+ onReplySend={() => void sendReply(item)}
182
+ onReplyToggle={() => {
183
+ if (activeReplyId === item.id) {
184
+ setActiveReplyId(null);
185
+ setReplyDraft("");
186
+ return;
187
+ }
188
+ setActiveReplyId(item.id);
189
+ setReplyDraft("");
190
+ }}
191
+ replyDraft={activeReplyId === item.id ? replyDraft : ""}
192
+ />
193
+ ))}
194
+ </section>
195
+ </div>
196
+ {isSendingReply ? (
197
+ <p className={timestampClass}>Sending reply...</p>
198
+ ) : null}
199
+ </main>
200
+ );
201
+ }
@@ -0,0 +1,125 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { TimelineCard } from "#/components/TimelineCard";
4
+ import type {
5
+ QueryEnvelope,
6
+ QueryResponse,
7
+ ReplyFilter,
8
+ TimelineItem,
9
+ } from "#/lib/types";
10
+ import {
11
+ 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,
25
+ } from "#/lib/ui";
26
+
27
+ export const Route = createFileRoute("/")({
28
+ component: HomeRoute,
29
+ });
30
+
31
+ function HomeRoute() {
32
+ const [meta, setMeta] = useState<QueryEnvelope | null>(null);
33
+ const [items, setItems] = useState<TimelineItem[]>([]);
34
+ const [replyFilter, setReplyFilter] = useState<ReplyFilter>("all");
35
+ const [search, setSearch] = useState("");
36
+ const [refreshTick, setRefreshTick] = useState(0);
37
+
38
+ useEffect(() => {
39
+ fetch("/api/status")
40
+ .then((response) => response.json())
41
+ .then((data: QueryEnvelope) => setMeta(data));
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ const url = new URL("/api/query", window.location.origin);
46
+ url.searchParams.set("resource", "home");
47
+ url.searchParams.set("replyFilter", replyFilter);
48
+ url.searchParams.set("refresh", String(refreshTick));
49
+ if (search.trim()) {
50
+ url.searchParams.set("search", search.trim());
51
+ }
52
+
53
+ fetch(url)
54
+ .then((response) => response.json())
55
+ .then((data: QueryResponse) => setItems(data.items as TimelineItem[]));
56
+ }, [refreshTick, replyFilter, search]);
57
+
58
+ const subtitle = useMemo(() => {
59
+ if (!meta) return "Loading local context...";
60
+ return `${meta.stats.home} home items · ${meta.stats.needsReply} waiting on action · ${meta.transport.statusText}`;
61
+ }, [meta]);
62
+
63
+ async function replyToTweet(tweetId: string) {
64
+ const text = window.prompt("Reply text");
65
+ if (!text?.trim()) return;
66
+
67
+ await fetch("/api/action", {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json" },
70
+ body: JSON.stringify({
71
+ kind: "replyTweet",
72
+ accountId: "acct_primary",
73
+ tweetId,
74
+ text,
75
+ }),
76
+ });
77
+
78
+ setRefreshTick((value) => value + 1);
79
+ }
80
+
81
+ return (
82
+ <main className={pageWrapClass}>
83
+ <div className={feedPageClass}>
84
+ <section className={heroShellClass}>
85
+ <div>
86
+ <p className={eyebrowClass}>home timeline</p>
87
+ <h2 className={heroTitleClass}>
88
+ Read first. Act only where signal survives.
89
+ </h2>
90
+ <p className={heroCopyClass}>{subtitle}</p>
91
+ </div>
92
+ <div className={heroControlsClass}>
93
+ <input
94
+ className={cx(textFieldClass, textFieldWideClass)}
95
+ onChange={(event) => setSearch(event.target.value)}
96
+ placeholder="Search local timeline"
97
+ value={search}
98
+ />
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>
124
+ );
125
+ }
@@ -0,0 +1,125 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { TimelineCard } from "#/components/TimelineCard";
4
+ import type {
5
+ QueryEnvelope,
6
+ QueryResponse,
7
+ ReplyFilter,
8
+ TimelineItem,
9
+ } from "#/lib/types";
10
+ import {
11
+ 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,
25
+ } from "#/lib/ui";
26
+
27
+ export const Route = createFileRoute("/mentions")({
28
+ component: MentionsRoute,
29
+ });
30
+
31
+ function MentionsRoute() {
32
+ const [meta, setMeta] = useState<QueryEnvelope | null>(null);
33
+ const [items, setItems] = useState<TimelineItem[]>([]);
34
+ const [replyFilter, setReplyFilter] = useState<ReplyFilter>("unreplied");
35
+ const [search, setSearch] = useState("");
36
+ const [refreshTick, setRefreshTick] = useState(0);
37
+
38
+ useEffect(() => {
39
+ fetch("/api/status")
40
+ .then((response) => response.json())
41
+ .then((data: QueryEnvelope) => setMeta(data));
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ const url = new URL("/api/query", window.location.origin);
46
+ url.searchParams.set("resource", "mentions");
47
+ url.searchParams.set("replyFilter", replyFilter);
48
+ url.searchParams.set("refresh", String(refreshTick));
49
+ if (search.trim()) {
50
+ url.searchParams.set("search", search.trim());
51
+ }
52
+
53
+ fetch(url)
54
+ .then((response) => response.json())
55
+ .then((data: QueryResponse) => setItems(data.items as TimelineItem[]));
56
+ }, [refreshTick, replyFilter, search]);
57
+
58
+ const subtitle = useMemo(() => {
59
+ if (!meta) return "Loading mentions...";
60
+ return `${meta.stats.mentions} mention/reply items in local store`;
61
+ }, [meta]);
62
+
63
+ async function replyToTweet(tweetId: string) {
64
+ const text = window.prompt("Reply text");
65
+ if (!text?.trim()) return;
66
+
67
+ await fetch("/api/action", {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json" },
70
+ body: JSON.stringify({
71
+ kind: "replyTweet",
72
+ accountId: "acct_primary",
73
+ tweetId,
74
+ text,
75
+ }),
76
+ });
77
+
78
+ setRefreshTick((value) => value + 1);
79
+ }
80
+
81
+ 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>
91
+ </div>
92
+ <div className={heroControlsClass}>
93
+ <input
94
+ className={cx(textFieldClass, textFieldWideClass)}
95
+ onChange={(event) => setSearch(event.target.value)}
96
+ placeholder="Search mentions"
97
+ value={search}
98
+ />
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>
124
+ );
125
+ }
package/src/styles.css ADDED
@@ -0,0 +1,109 @@
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
+ @import "tailwindcss";
3
+
4
+ @theme inline {
5
+ --font-sans: "Instrument Sans", sans-serif;
6
+ --font-display: "Fraunces", serif;
7
+ }
8
+
9
+ :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%);
22
+ --theme-switch-x: 50%;
23
+ --theme-switch-y: 50%;
24
+ color-scheme: light;
25
+ }
26
+
27
+ [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%);
40
+ color-scheme: dark;
41
+ }
42
+
43
+ html,
44
+ body {
45
+ min-height: 100%;
46
+ }
47
+
48
+ 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
+ );
65
+ }
66
+
67
+ @keyframes theme-circle-transition {
68
+ 0% {
69
+ clip-path: circle(
70
+ 0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
71
+ );
72
+ }
73
+
74
+ 100% {
75
+ clip-path: circle(
76
+ 150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
77
+ );
78
+ }
79
+ }
80
+
81
+ html.theme-transition {
82
+ view-transition-name: theme;
83
+ }
84
+
85
+ /* View transition pseudo-element. */
86
+ html.theme-transition::view-transition-old(theme) {
87
+ animation: none;
88
+ mix-blend-mode: normal;
89
+ z-index: 1;
90
+ }
91
+
92
+ /* View transition pseudo-element. */
93
+ html.theme-transition::view-transition-new(theme) {
94
+ animation: theme-circle-transition 0.45s ease-out forwards;
95
+ mix-blend-mode: normal;
96
+ z-index: 2;
97
+ }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ /* View transition pseudo-element. */
101
+ html.theme-transition::view-transition-old(theme) {
102
+ animation: none !important;
103
+ }
104
+
105
+ /* View transition pseudo-element. */
106
+ html.theme-transition::view-transition-new(theme) {
107
+ animation: none !important;
108
+ }
109
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "jsx": "react-jsx",
6
+ "module": "ESNext",
7
+ "baseUrl": ".",
8
+ "ignoreDeprecations": "6.0",
9
+ "paths": {
10
+ "#/*": ["./src/*"],
11
+ "@/*": ["./src/*"]
12
+ },
13
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
14
+ "types": ["vite/client"],
15
+
16
+ /* Bundler mode */
17
+ "moduleResolution": "bundler",
18
+ "allowImportingTsExtensions": true,
19
+ "verbatimModuleSyntax": true,
20
+ "noEmit": true,
21
+
22
+ /* Linting */
23
+ "skipLibCheck": true,
24
+ "strict": true,
25
+ "noUnusedLocals": true,
26
+ "noUnusedParameters": true,
27
+ "noFallthroughCasesInSwitch": true,
28
+ "noUncheckedSideEffectImports": true
29
+ }
30
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import { devtools } from "@tanstack/devtools-vite";
3
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
4
+ import viteReact from "@vitejs/plugin-react";
5
+ import { defineConfig } from "vite";
6
+
7
+ const config = defineConfig({
8
+ plugins: [
9
+ devtools(),
10
+ tailwindcss(),
11
+ tanstackStart({
12
+ router: {
13
+ routeFileIgnorePattern: "\\.(test|spec)\\.(ts|tsx)$",
14
+ },
15
+ }),
16
+ viteReact(),
17
+ ],
18
+ resolve: {
19
+ tsconfigPaths: true,
20
+ },
21
+ });
22
+
23
+ export default config;