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
package/src/lib/ui.ts CHANGED
@@ -3,268 +3,496 @@ export function cx(...values: Array<string | false | null | undefined>) {
3
3
  }
4
4
 
5
5
  export const bodyClass =
6
- "min-h-screen bg-[var(--bg)] font-sans text-[var(--ink)] antialiased transition-[background,color] duration-200";
6
+ "min-h-screen bg-[var(--bg)] font-sans text-[var(--ink)] antialiased text-[15px] leading-[1.4]";
7
7
 
8
+ /* App shell — sidebar left, single feed column center, optional aside right. */
8
9
  export const siteShellClass =
9
- "mx-auto w-[min(1120px,calc(100vw-32px))] pb-12 pt-[18px] max-[760px]:w-[min(100vw-20px,100%)]";
10
+ "mx-auto flex min-h-screen w-full max-w-[1280px] gap-0";
10
11
 
11
- export const pageWrapClass = "pt-6";
12
+ export const sidebarShellClass =
13
+ "sticky top-0 z-30 flex h-screen w-[72px] shrink-0 flex-col justify-between border-r border-[var(--line)] bg-[var(--bg)] px-2 py-3 min-[1100px]:w-[260px] min-[1100px]:px-3";
12
14
 
13
- export const feedPageClass = "mx-auto grid w-full max-w-[960px] gap-6";
15
+ export const sidebarBrandClass =
16
+ "flex items-center gap-2.5 px-2 py-2 text-[var(--ink)] min-[1100px]:px-3";
14
17
 
15
- export const timelineLaneClass = "mx-auto grid w-full max-w-[680px] gap-4";
18
+ export const sidebarBrandMarkClass =
19
+ "grid size-9 place-items-center rounded-full bg-[var(--accent)] text-white";
16
20
 
17
- export const inboxLaneClass = "mx-auto grid w-full max-w-[760px] gap-4";
21
+ export const sidebarBrandCopyClass =
22
+ "hidden flex-col leading-tight min-[1100px]:flex";
18
23
 
19
- export const dmPageClass = "mx-auto grid w-full max-w-[1040px] gap-6";
24
+ export const sidebarBrandTitleClass = "text-[15px] font-bold tracking-tight";
20
25
 
21
- export const eyebrowClass =
22
- "m-0 mb-2 text-[11px] uppercase tracking-[0.24em] text-[var(--ink-soft)]";
23
-
24
- export const brandMarkClass =
25
- "m-0 max-w-[12ch] font-display text-[clamp(1.8rem,3vw,3.2rem)] leading-[0.95] font-semibold";
26
+ export const sidebarBrandTaglineClass = "text-[12px] text-[var(--ink-soft)]";
26
27
 
27
- export const navClass =
28
- "flex items-end justify-between gap-6 border-b border-[var(--line)] py-[18px] pb-5 max-[760px]:flex-col max-[760px]:items-start";
28
+ export const sidebarNavClass = "mt-2 flex flex-col gap-0.5";
29
29
 
30
- export const navLinksClass = "nav-links flex flex-wrap items-center gap-2.5";
30
+ export const sidebarFooterClass = "flex flex-col gap-2 pb-1";
31
31
 
32
32
  export const navLinkClass =
33
- "nav-link inline-flex items-center rounded-full border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_76%,transparent)] px-4 py-2.5 text-[var(--ink-soft)] transition duration-180 hover:-translate-y-px hover:border-[var(--line-strong)] hover:text-[var(--ink)]";
33
+ "nav-link group flex items-center justify-center gap-4 rounded-full px-3 py-2.5 text-[15px] text-[var(--ink)] transition-colors duration-150 hover:bg-[var(--bg-hover)] min-[1100px]:justify-start";
34
34
 
35
- export const navLinkActiveClass =
36
- "nav-link-active border-[var(--line-strong)] bg-[var(--panel)] text-[var(--ink)] shadow-[0_16px_42px_var(--shadow)]";
35
+ export const navLinkActiveClass = "nav-link-active font-bold";
37
36
 
38
- export const heroShellClass =
39
- "flex items-end justify-between gap-6 py-7 pb-6 max-[760px]:flex-col max-[760px]:items-start";
37
+ export const navLinkIconClass = "shrink-0";
40
38
 
41
- export const heroShellDmClass = "items-start";
39
+ export const navLinkLabelClass = "sr-only min-[1100px]:not-sr-only";
42
40
 
43
- export const heroTitleClass =
44
- "m-0 max-w-[12ch] font-display text-[clamp(2.2rem,4vw,4.4rem)] leading-[0.95]";
41
+ /* Main column. */
42
+ export const mainColumnClass =
43
+ "flex w-full min-w-0 max-w-[680px] flex-1 flex-col border-x border-[var(--line)] bg-[var(--bg)]";
45
44
 
46
- export const heroCopyClass =
47
- "hero-copy mt-3.5 max-w-[46ch] text-base text-[var(--ink-soft)]";
45
+ export const pageWrapClass = "flex w-full min-w-0 flex-1 justify-start";
48
46
 
49
- export const heroControlsClass =
50
- "flex flex-wrap justify-end gap-3 max-[760px]:w-full max-[760px]:justify-start";
47
+ /* Sticky page header at top of the main column. */
48
+ export const pageHeaderClass =
49
+ "sticky top-0 z-20 flex flex-col gap-0 border-b border-[var(--line)] bg-[color:color-mix(in_srgb,var(--bg)_85%,transparent)] backdrop-blur";
51
50
 
52
- export const heroControlsDmClass = "max-w-[420px]";
51
+ export const pageHeaderRowClass =
52
+ "flex items-center justify-between gap-3 px-4 pt-3 pb-2";
53
53
 
54
- export const heroControlsBlocksClass = "max-w-[560px]";
54
+ export const pageTitleClass = "m-0 text-[20px] font-bold tracking-tight";
55
55
 
56
- export const textFieldClass =
57
- "text-field w-full rounded-2xl border border-[var(--line)] bg-[var(--panel)] px-3.5 py-3 text-[var(--ink)] shadow-[0_18px_40px_var(--shadow)] outline-none transition duration-180 placeholder:text-[var(--ink-soft)] focus:border-[var(--line-strong)] disabled:cursor-default disabled:opacity-55";
56
+ export const pageSubtitleClass = "text-[13px] text-[var(--ink-soft)]";
58
57
 
59
- export const textFieldWideClass = "w-[min(360px,100%)] max-[760px]:w-full";
58
+ export const pageHeaderActionsClass = "flex items-center gap-2";
60
59
 
61
- export const textFieldShortClass =
62
- "text-field-short w-[140px] max-[760px]:w-full";
60
+ /* Sticky tab strip Twitter-style. */
61
+ export const tabStripClass =
62
+ "flex w-full items-stretch border-b border-[var(--line)] overflow-x-auto";
63
63
 
64
- export const segmentedClass =
65
- "inline-flex gap-1.5 rounded-full border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_76%,transparent)] p-1.5";
64
+ export const tabButtonClass =
65
+ "relative flex flex-1 min-w-[88px] items-center justify-center px-3 py-3.5 text-[14px] font-medium text-[var(--ink-soft)] transition-colors duration-150 hover:bg-[var(--bg-hover)]";
66
66
 
67
- export const segmentClass =
68
- "segment rounded-full border-0 bg-transparent px-3.5 py-2.5 text-[var(--ink-soft)] transition duration-180";
67
+ export const tabButtonActiveClass = "text-[var(--ink)] font-bold";
69
68
 
70
- export const segmentActiveClass =
71
- "segment-active bg-[var(--panel)] text-[var(--ink)] shadow-[0_8px_22px_var(--shadow)]";
69
+ export const tabButtonIndicatorClass =
70
+ "pointer-events-none absolute inset-x-0 bottom-0 mx-auto h-[3px] w-12 rounded-full bg-[var(--accent)]";
72
71
 
73
- export const stackGridClass = "grid gap-4";
72
+ /* Feed rows: flat, hairline divider, no boxed cards. */
73
+ export const feedClass = "flex flex-col";
74
74
 
75
- export const surfaceCardClass =
76
- "rounded-[24px] border border-[var(--line)] bg-[var(--panel)] shadow-[0_24px_60px_var(--shadow)]";
75
+ export const feedRowClass =
76
+ "flex w-full gap-3 border-b border-[var(--line)] px-4 py-3 transition-colors duration-150 hover:bg-[var(--bg-hover)]";
77
77
 
78
- export const contentCardClass = cx("content-card", surfaceCardClass, "p-5");
78
+ export const feedRowBodyClass = "flex min-w-0 flex-1 flex-col gap-1";
79
79
 
80
- export const cardHeaderClass =
81
- "flex items-start justify-between gap-3 max-[760px]:flex-col max-[760px]:items-start";
80
+ export const feedRowHeaderClass =
81
+ "flex min-w-0 items-center gap-1.5 text-[15px]";
82
82
 
83
- export const cardFooterClass =
84
- "flex items-center justify-between gap-3 max-[760px]:flex-col max-[760px]:items-start";
83
+ export const feedRowNameClass =
84
+ "truncate font-bold text-[var(--ink)] hover:underline";
85
85
 
86
- export const identityBlockClass = "flex items-start gap-3.5";
86
+ export const feedRowHandleClass = "truncate text-[var(--ink-soft)]";
87
87
 
88
- export const identityRowClass =
89
- "flex flex-wrap items-center gap-2.5 text-[0.92rem] text-[var(--ink-soft)]";
88
+ export const feedRowDotClass = "text-[var(--ink-soft)]";
90
89
 
91
- export const metaRowClass =
92
- "flex flex-wrap items-center gap-2.5 text-[0.92rem] text-[var(--ink-soft)]";
90
+ export const feedRowTimestampClass =
91
+ "shrink-0 text-[var(--ink-soft)] hover:underline";
93
92
 
94
- export const mutedDotClass =
95
- "muted-dot inline-block size-1 rounded-full bg-[var(--line-strong)]";
93
+ export const feedRowMetaClass =
94
+ "flex flex-wrap items-center gap-2 text-[13px] text-[var(--ink-soft)]";
96
95
 
97
- export const metaStackClass = "flex flex-col items-end gap-2";
96
+ export const feedRowActionsClass =
97
+ "mt-2 flex max-w-md items-center justify-between text-[var(--ink-soft)]";
98
98
 
99
- export const pillClass =
100
- "pill inline-flex items-center rounded-full px-3 py-[7px] text-[0.76rem] uppercase tracking-[0.08em]";
99
+ export const feedActionButtonClass =
100
+ "action-button group/action inline-flex items-center gap-1 rounded-full border-0 bg-transparent px-2 py-1 text-[13px] text-[var(--ink-soft)] transition-colors duration-150 hover:text-[var(--accent)] disabled:cursor-default disabled:opacity-55";
101
101
 
102
- export const pillSoftClass =
103
- "pill-soft bg-[var(--accent-soft)] text-[var(--accent)]";
102
+ export const feedActionLikeClass = "hover:text-[var(--like)]";
104
103
 
105
- export const pillAlertClass =
106
- "pill-alert bg-[var(--alert-soft)] text-[var(--alert)]";
104
+ export const feedActionIconWrapClass =
105
+ "grid size-[34px] place-items-center rounded-full transition-colors duration-150 group-hover/action:bg-[var(--accent-soft)]";
107
106
 
108
- export const timestampClass = "timestamp text-[0.9rem] text-[var(--ink-soft)]";
107
+ export const feedActionIconWrapLikeClass =
108
+ "group-hover/action:bg-[var(--like-soft)]";
109
109
 
110
- export const bodyCopyClass = "body-copy my-4 text-[1.08rem] leading-[1.6]";
110
+ export const feedActionIconClass = "size-[18px]";
111
111
 
112
- export const inboxTitleClass = "inbox-title m-0 font-display text-[1.4rem]";
112
+ export const feedRowTextClass =
113
+ "whitespace-pre-wrap break-words text-[15px] leading-[1.45] text-[var(--ink)] [overflow-wrap:anywhere]";
113
114
 
114
- export const inboxAnalysisClass =
115
- "inbox-analysis pb-1 pt-4 text-[var(--ink-soft)] [&_p]:mt-2 [&_p]:mb-0";
115
+ export const feedRowBadgeClass =
116
+ "inline-flex items-center gap-1 rounded-full bg-[var(--accent-soft)] px-2 py-0.5 text-[12px] font-semibold text-[var(--accent)]";
116
117
 
117
- export const metricRowClass =
118
- "metric-row flex flex-wrap gap-3.5 text-[0.9rem] text-[var(--ink-soft)]";
118
+ export const feedRowBadgeAlertClass =
119
+ "bg-[var(--alert-soft)] text-[var(--alert)]";
119
120
 
120
- export const actionRowClass = "flex items-center gap-2.5";
121
+ export const feedRowBadgeNeutralClass =
122
+ "bg-[var(--bg-active)] text-[var(--ink-soft)]";
121
123
 
122
- export const actionButtonClass =
123
- "action-button inline-flex items-center justify-center rounded-full border-0 bg-[var(--ink)] px-4 py-[11px] text-sm font-medium text-white transition duration-180 hover:-translate-y-px disabled:cursor-default disabled:opacity-55";
124
+ /* Forms / inputs. */
125
+ export const searchFieldShellClass =
126
+ "flex items-center gap-2 rounded-full border border-transparent bg-[var(--bg-active)] px-4 py-2 transition-colors focus-within:border-[var(--accent)] focus-within:bg-[var(--bg)] focus-within:shadow-[0_0_0_1px_var(--accent)]";
127
+
128
+ export const searchFieldIconClass =
129
+ "size-[18px] text-[var(--ink-soft)] shrink-0";
130
+
131
+ export const searchFieldInputClass =
132
+ "min-w-0 flex-1 border-0 bg-transparent text-[14px] text-[var(--ink)] outline-none placeholder:text-[var(--ink-soft)]";
133
+
134
+ export const textFieldClass =
135
+ "w-full rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 py-2 text-[14px] text-[var(--ink)] outline-none transition-colors duration-150 placeholder:text-[var(--ink-soft)] focus:border-[var(--accent)] focus:shadow-[0_0_0_1px_var(--accent)] disabled:cursor-default disabled:opacity-55";
124
136
 
125
- export const errorCopyClass = "mb-4 text-[var(--alert)]";
137
+ export const textFieldWideClass = "w-full";
126
138
 
139
+ export const textFieldShortClass = "w-[120px]";
140
+
141
+ export const selectFieldClass = cx(textFieldClass, "appearance-none pr-8");
142
+
143
+ /* Buttons. */
144
+ export const primaryButtonClass =
145
+ "inline-flex items-center justify-center gap-1.5 rounded-full border-0 bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white shadow-none transition-colors duration-150 hover:bg-[var(--accent-hover)] active:bg-[var(--accent-press)] disabled:cursor-default disabled:opacity-55";
146
+
147
+ export const primaryButtonLargeClass =
148
+ "inline-flex items-center justify-center gap-1.5 rounded-full border-0 bg-[var(--accent)] px-5 py-2.5 text-[15px] font-bold text-white shadow-none transition-colors duration-150 hover:bg-[var(--accent-hover)] active:bg-[var(--accent-press)] disabled:cursor-default disabled:opacity-55";
149
+
150
+ export const secondaryButtonClass =
151
+ "inline-flex items-center justify-center gap-1.5 rounded-full border border-[var(--line-strong)] bg-transparent px-4 py-1.5 text-[14px] font-bold text-[var(--ink)] transition-colors duration-150 hover:bg-[var(--bg-hover)] disabled:cursor-default disabled:opacity-55";
152
+
153
+ export const dangerButtonClass =
154
+ "inline-flex items-center justify-center gap-1.5 rounded-full border border-[var(--alert)] bg-transparent px-4 py-1.5 text-[14px] font-bold text-[var(--alert)] transition-colors duration-150 hover:bg-[var(--alert-soft)] disabled:cursor-default disabled:opacity-55";
155
+
156
+ export const actionButtonClass = primaryButtonClass;
157
+
158
+ /* Legacy segmented control retained for routes that still use it (compact pill). */
159
+ export const segmentedClass =
160
+ "inline-flex shrink-0 items-center gap-1 rounded-full border border-[var(--line)] bg-[var(--bg)] p-1";
161
+
162
+ export const segmentClass =
163
+ "rounded-full border-0 bg-transparent px-3 py-1 text-[13px] font-medium text-[var(--ink-soft)] capitalize transition-colors duration-150";
164
+
165
+ export const segmentActiveClass = "bg-[var(--bg-active)] text-[var(--ink)]";
166
+
167
+ /* Composer (reply textarea). */
127
168
  export const composerShellClass =
128
- "mt-2.5 border-t border-[var(--line)] pt-[18px]";
169
+ "mt-2 flex flex-col gap-2 border-t border-[var(--line)] pt-3";
129
170
 
130
171
  export const composerInputClass =
131
- "composer-input min-h-[120px] w-full resize-y rounded-[18px] border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_88%,transparent)] px-4 py-4 text-[var(--ink)] shadow-[inset_0_1px_0_color-mix(in_srgb,white_36%,transparent)] outline-none transition duration-180 placeholder:text-[var(--ink-soft)] focus:border-[var(--line-strong)]";
172
+ "min-h-[88px] w-full resize-y rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 py-2.5 text-[15px] text-[var(--ink)] outline-none transition-colors duration-150 placeholder:text-[var(--ink-soft)] focus:border-[var(--accent)] focus:shadow-[0_0_0_1px_var(--accent)]";
132
173
 
133
- export const composerBarClass =
134
- "flex items-center justify-between gap-4 pt-3 max-[760px]:flex-col max-[760px]:items-start";
174
+ export const composerBarClass = "flex items-center justify-between gap-3";
135
175
 
136
- export const linkPreviewCardClass =
137
- "grid gap-2.5 rounded-[20px] border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_84%,transparent)] px-4 py-3.5";
176
+ /* Empty / loading. */
177
+ export const emptyStateClass = "px-6 py-10 text-center text-[var(--ink-soft)]";
138
178
 
139
- export const embeddedTweetCardClass = linkPreviewCardClass;
179
+ export const errorCopyClass =
180
+ "mx-4 my-3 whitespace-pre-wrap break-words rounded-md border border-[var(--alert)] bg-[var(--alert-soft)] px-3 py-2 text-[14px] text-[var(--alert)]";
140
181
 
141
- export const embeddedTweetLabelClass =
142
- "embedded-tweet-label m-0 text-[0.78rem] uppercase tracking-[0.12em] text-[var(--ink-soft)]";
182
+ export const statusCopyClass =
183
+ "mx-4 my-3 rounded-md border border-[var(--line)] bg-[var(--bg-elevated)] px-3 py-2 text-[13px] text-[var(--ink-soft)]";
143
184
 
144
- export const embeddedTweetHeaderClass =
145
- "embedded-tweet-header flex items-center justify-between gap-2.5";
185
+ /* Avatars. */
186
+ export const avatarChipClass =
187
+ "avatar-chip inline-grid size-10 shrink-0 place-items-center overflow-hidden rounded-full text-[14px] font-bold text-white";
146
188
 
147
- export const embeddedTweetAuthorClass =
148
- "embedded-tweet-author flex flex-wrap items-center gap-2.5 text-[var(--ink-soft)]";
189
+ export const avatarChipLargeClass = "avatar-chip-large size-16 text-[18px]";
149
190
 
150
- export const embeddedTweetCopyClass =
151
- "embedded-tweet-copy m-0 text-[0.98rem] leading-[1.55]";
191
+ export const avatarChipSmallClass = "avatar-chip-small size-8 text-[12px]";
152
192
 
153
- export const dmGridClass =
154
- "grid items-start gap-4 min-[980px]:grid-cols-[minmax(280px,320px)_minmax(0,1fr)] min-[1120px]:gap-5";
193
+ /* Embedded tweet / link preview (rounded box, used inside feed row body). */
194
+ export const embeddedCardClass =
195
+ "mt-2 overflow-hidden rounded-2xl border border-[var(--line)] transition-colors duration-150 hover:bg-[var(--bg-hover)]";
155
196
 
156
- export const dmListClass = cx("dm-list", surfaceCardClass, "p-2.5");
197
+ export const embeddedCardBodyClass = "flex flex-col gap-2 px-3 py-2.5";
157
198
 
158
- export const dmListItemClass =
159
- "dm-list-item grid w-full grid-cols-[auto_1fr_auto] gap-3 rounded-[18px] border-0 bg-transparent p-3.5 text-left transition duration-180 hover:bg-[color:color-mix(in_srgb,var(--panel-strong)_72%,transparent)]";
199
+ export const embeddedCardHeaderClass = "flex items-center gap-1.5 text-[14px]";
160
200
 
161
- export const dmListItemActiveClass =
162
- "dm-list-item-active bg-[color:color-mix(in_srgb,var(--panel-strong)_86%,transparent)]";
201
+ export const embeddedCardNameClass = "font-bold text-[var(--ink)] truncate";
163
202
 
164
- export const dmListCopyClass = "dm-list-copy min-w-0";
203
+ export const embeddedCardHandleClass = "text-[var(--ink-soft)] truncate";
165
204
 
166
- export const dmPreviewTextClass =
167
- "mt-1.5 overflow-hidden text-ellipsis whitespace-nowrap text-[var(--ink-soft)]";
205
+ export const embeddedCardLabelClass =
206
+ "text-[12px] uppercase tracking-wide text-[var(--ink-soft)]";
168
207
 
169
- export const threadShellClass =
170
- "thread-shell min-h-[70vh] rounded-[24px] border border-[var(--line)] bg-[var(--panel)] p-[18px] shadow-[0_24px_60px_var(--shadow)]";
208
+ export const embeddedCardCopyClass =
209
+ "whitespace-pre-wrap text-[14px] leading-[1.4] text-[var(--ink)] [overflow-wrap:anywhere]";
171
210
 
172
- export const threadHeaderClass =
173
- "thread-header grid gap-5 border-b border-[var(--line)] pb-4 min-[860px]:grid-cols-[minmax(0,1fr)_minmax(220px,260px)]";
211
+ export const linkPreviewCardClass =
212
+ "group/link-preview mt-2 flex min-w-0 overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] transition-colors duration-150 hover:bg-[var(--bg-hover)]";
174
213
 
175
- export const threadTitleClass = "m-0 font-display text-[2rem]";
214
+ export const linkPreviewTitleClass =
215
+ "line-clamp-2 font-bold leading-tight text-[var(--ink)]";
176
216
 
177
- export const threadSubtitleClass = "mt-1.5 text-[var(--ink-soft)]";
217
+ export const linkPreviewDescClass =
218
+ "line-clamp-2 text-[14px] leading-snug text-[var(--ink-soft)]";
178
219
 
179
- export const threadBioClass =
180
- "mt-2.5 max-w-[48ch] text-[var(--ink-soft)] leading-[1.55]";
220
+ export const linkPreviewHostClass =
221
+ "text-[13px] text-[var(--ink-soft)] truncate";
181
222
 
182
- export const threadDetailColumnClass =
183
- "grid gap-3 rounded-[20px] border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_82%,transparent)] p-4";
223
+ /* DM grid. */
224
+ export const dmShellClass =
225
+ "grid h-[calc(100vh-56px)] min-h-[520px] grid-cols-1 min-[1100px]:grid-cols-[320px_minmax(0,1fr)]";
184
226
 
185
- export const threadDetailHeaderClass = "flex items-center gap-3";
227
+ export const dmListClass =
228
+ "flex flex-col overflow-y-auto border-r border-[var(--line)]";
229
+
230
+ export const dmListItemClass =
231
+ "flex w-full items-start gap-3 border-b border-[var(--line)] border-0 bg-transparent px-3 py-3 text-left transition-colors duration-150 hover:bg-[var(--bg-hover)]";
186
232
 
187
- export const messageStackClass = "flex flex-col gap-3.5 py-5 pb-[18px]";
233
+ export const dmListItemActiveClass = "bg-[var(--bg-active)]";
188
234
 
189
- export const messageRowClass = "message-row max-w-[80%] max-[760px]:max-w-full";
235
+ export const dmListBodyClass = "flex min-w-0 flex-1 flex-col gap-1";
190
236
 
191
- export const messageRowOutboundClass = "message-row-outbound self-end";
237
+ export const dmListHeaderClass = "flex items-center justify-between gap-2";
192
238
 
193
- export const messageMetaClass =
194
- "message-meta mb-2 flex justify-between gap-3 text-sm text-[var(--ink-soft)]";
239
+ export const dmListNameClass =
240
+ "truncate text-[14px] font-bold text-[var(--ink)]";
195
241
 
196
- export const messageBubbleClass =
197
- "rounded-[18px] rounded-bl-[6px] bg-[color:color-mix(in_srgb,var(--panel-strong)_86%,transparent)] px-4 py-3.5 leading-[1.55]";
242
+ export const dmListHandleClass = "truncate text-[13px] text-[var(--ink-soft)]";
198
243
 
199
- export const messageBubbleOutboundClass =
200
- "rounded-bl-[18px] rounded-br-[6px] bg-[var(--accent-soft)]";
244
+ export const dmListPreviewClass = "truncate text-[13px] text-[var(--ink-soft)]";
201
245
 
202
- export const contextHandleClass = "mt-1.5 text-[var(--ink-soft)]";
246
+ export const dmListTimestampClass =
247
+ "shrink-0 text-[12px] text-[var(--ink-soft)]";
203
248
 
204
- export const contextBioClass = bodyCopyClass;
249
+ export const dmThreadClass = "flex min-w-0 flex-col";
205
250
 
206
- export const contextStatsClass = "mt-[18px] grid gap-3";
251
+ export const dmThreadHeaderClass =
252
+ "sticky top-0 z-10 flex items-center justify-between gap-3 border-b border-[var(--line)] bg-[color:color-mix(in_srgb,var(--bg)_85%,transparent)] px-4 py-3 backdrop-blur";
207
253
 
208
- export const contextStatRowClass = "border-t border-[var(--line)] pt-3";
254
+ export const dmThreadTitleClass = "flex items-center gap-3 min-w-0";
209
255
 
210
- export const contextStatTermClass = "text-[0.84rem] text-[var(--ink-soft)]";
256
+ export const dmThreadNameClass = "truncate text-[16px] font-bold";
211
257
 
212
- export const contextStatValueClass = "mt-1.5 font-semibold";
258
+ export const dmThreadSubtitleClass =
259
+ "truncate text-[13px] text-[var(--ink-soft)]";
213
260
 
214
- export const avatarChipClass =
215
- "avatar-chip inline-grid size-[42px] place-items-center overflow-hidden rounded-2xl font-bold text-white";
261
+ export const dmMessagesClass =
262
+ "flex flex-1 flex-col gap-2 overflow-y-auto px-4 py-4";
263
+
264
+ export const dmMessageRowClass = "flex max-w-[78%] flex-col gap-1";
265
+
266
+ export const dmMessageRowOutboundClass = "self-end items-end";
216
267
 
217
- export const avatarChipLargeClass =
218
- "avatar-chip-large mb-3.5 size-16 rounded-[20px]";
268
+ export const dmMessageBubbleClass =
269
+ "rounded-2xl rounded-bl-md bg-[var(--bg-active)] px-3.5 py-2 text-[15px] leading-[1.4] text-[var(--ink)]";
219
270
 
220
- export const emptyStateClass = "py-6 text-[var(--ink-soft)]";
271
+ export const dmMessageBubbleOutboundClass =
272
+ "rounded-bl-2xl rounded-br-md bg-[var(--accent)] text-white";
221
273
 
222
- export const profilePreviewClass = "profile-preview relative inline-flex";
274
+ export const dmMessageMetaClass =
275
+ "flex items-center gap-2 text-[12px] text-[var(--ink-soft)]";
276
+
277
+ export const dmComposerShellClass =
278
+ "flex flex-col gap-2 border-t border-[var(--line)] px-4 py-3";
279
+
280
+ /* Profile hovercard. */
281
+ export const profilePreviewClass = "relative inline-flex";
223
282
 
224
283
  export const profilePreviewTriggerClass =
225
284
  "profile-preview-trigger inline-flex text-inherit";
226
285
 
227
286
  export const profilePreviewCardClass =
228
- "profile-preview-card pointer-events-none absolute left-0 top-[calc(100%+10px)] z-20 grid min-w-[240px] translate-y-1.5 gap-2.5 rounded-[18px] border border-[var(--line-strong)] bg-[var(--panel-strong)] p-3.5 opacity-0 shadow-[0_24px_60px_var(--shadow)] transition duration-160 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100";
287
+ "pointer-events-none absolute left-0 top-[calc(100%+8px)] z-30 grid w-[280px] translate-y-1 gap-2 rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)] p-3 opacity-0 shadow-[0_8px_28px_var(--shadow-strong)] transition-all duration-150 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100";
229
288
 
230
- export const profilePreviewHeaderClass =
231
- "profile-preview-header flex items-center gap-3";
289
+ export const profilePreviewHeaderClass = "flex items-center gap-3";
290
+
291
+ export const profilePreviewNameClass =
292
+ "block text-[15px] font-bold text-[var(--ink)]";
232
293
 
233
294
  export const profilePreviewHandleClass =
234
- "profile-preview-handle block text-[0.88rem] text-[var(--ink-soft)]";
295
+ "block text-[14px] text-[var(--ink-soft)]";
235
296
 
236
297
  export const profilePreviewBioClass =
237
- "profile-preview-bio block text-[0.95rem] leading-[1.5] text-[var(--ink)]";
298
+ "block text-[14px] leading-[1.4] text-[var(--ink)]";
238
299
 
239
300
  export const profilePreviewMetaClass =
240
- "profile-preview-meta block text-[0.88rem] text-[var(--ink-soft)]";
301
+ "block text-[13px] text-[var(--ink-soft)]";
241
302
 
242
- export const tweetLinkClass =
243
- "tweet-link font-semibold text-[var(--accent)] underline decoration-[1px] underline-offset-[3px]";
303
+ /* Rich tweet text. */
304
+ export const tweetLinkClass = "text-[var(--accent)] hover:underline";
244
305
 
245
- export const tweetMentionClass =
246
- "tweet-mention font-semibold text-[color:color-mix(in_srgb,var(--accent)_78%,var(--ink))]";
306
+ export const tweetMentionClass = "text-[var(--accent)] hover:underline";
247
307
 
248
308
  export const tweetHashtagClass = tweetMentionClass;
249
309
 
310
+ /* Media grid (Twitter-style with rounded corners). */
250
311
  export function tweetMediaGridClass(count: number) {
251
- const columns =
312
+ const capped = Math.min(count, 4);
313
+ const layout =
252
314
  count === 1
253
315
  ? "grid-cols-1"
254
316
  : count === 3
255
317
  ? "grid-cols-[1.3fr_1fr]"
256
- : "grid-cols-2";
318
+ : count === 4
319
+ ? "grid-cols-2 grid-rows-2"
320
+ : "grid-cols-2";
257
321
 
258
322
  return cx(
259
- `tweet-media-grid tweet-media-grid-${Math.min(count, 4)}`,
260
- "mb-4 grid gap-2.5",
261
- columns,
323
+ `tweet-media-grid tweet-media-grid-${String(capped)}`,
324
+ "mt-2 grid gap-0.5 overflow-hidden rounded-2xl border border-[var(--line)]",
325
+ layout,
262
326
  );
263
327
  }
264
328
 
265
329
  export function tweetMediaTileClass(index: number, count: number) {
266
330
  return cx(
267
- "tweet-media-tile overflow-hidden rounded-[20px] border border-[var(--line)] bg-[color:color-mix(in_srgb,var(--panel-strong)_88%,transparent)]",
268
- count === 3 && index === 0 && "row-span-2",
331
+ "tweet-media-tile relative block overflow-hidden bg-[var(--bg-active)]",
332
+ count === 1 && "aspect-[16/10]",
333
+ count === 2 && "aspect-square",
334
+ count === 3 && index === 0 && "row-span-2 aspect-[3/4]",
335
+ count === 3 && index !== 0 && "aspect-square",
336
+ count === 4 && "aspect-square",
269
337
  );
270
338
  }
339
+
340
+ /* Inbox / triage card. */
341
+ export const inboxAnalysisClass =
342
+ "mt-2 rounded-xl border border-[var(--line)] bg-[var(--bg-hover)] px-3 py-2.5 text-[14px] text-[var(--ink-soft)] [&_p]:mt-1 [&_p]:mb-0";
343
+
344
+ /* Block list item. */
345
+ export const blockRowClass =
346
+ "flex items-start gap-3 border-b border-[var(--line)] px-4 py-3 transition-colors duration-150 hover:bg-[var(--bg-hover)]";
347
+
348
+ export const blockRowBodyClass = "flex min-w-0 flex-1 flex-col gap-1";
349
+
350
+ /* Aside (optional right column for stats). Hidden on small screens. */
351
+ export const asideShellClass =
352
+ "sticky top-0 hidden h-screen w-[300px] shrink-0 flex-col gap-4 overflow-y-auto px-4 py-4 min-[1180px]:flex";
353
+
354
+ export const asideCardClass =
355
+ "rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)] p-4";
356
+
357
+ export const asideTitleClass = "text-[15px] font-bold text-[var(--ink)]";
358
+
359
+ export const asideStatRowClass =
360
+ "flex items-baseline justify-between border-t border-[var(--line)] py-2 first:border-t-0 first:pt-0";
361
+
362
+ export const asideStatTermClass = "text-[13px] text-[var(--ink-soft)]";
363
+
364
+ export const asideStatValueClass = "text-[14px] font-bold text-[var(--ink)]";
365
+
366
+ /* Misc legacy aliases kept for compatibility while routes/components migrate. */
367
+ export const eyebrowClass =
368
+ "text-[12px] font-semibold uppercase tracking-wide text-[var(--ink-soft)]";
369
+
370
+ export const brandMarkClass = sidebarBrandTitleClass;
371
+
372
+ export const navClass = sidebarShellClass;
373
+
374
+ export const navLinksClass = sidebarNavClass;
375
+
376
+ export const heroShellClass = "flex flex-col gap-1 px-4 pt-3 pb-2";
377
+
378
+ export const heroShellDmClass = "";
379
+
380
+ export const heroControlsClass = "flex flex-wrap items-center gap-2 px-4 py-2";
381
+
382
+ export const heroControlsDmClass = "";
383
+
384
+ export const heroControlsBlocksClass = "";
385
+
386
+ export const heroTitleClass = pageTitleClass;
387
+
388
+ export const heroCopyClass = pageSubtitleClass;
389
+
390
+ export const surfaceCardClass =
391
+ "rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)]";
392
+
393
+ export const contentCardClass = feedRowClass;
394
+
395
+ export const cardHeaderClass = "flex items-start justify-between gap-3";
396
+
397
+ export const cardFooterClass = "flex items-center justify-between gap-3";
398
+
399
+ export const identityBlockClass = "flex items-start gap-3";
400
+
401
+ export const identityRowClass =
402
+ "flex flex-wrap items-center gap-1.5 text-[15px] text-[var(--ink-soft)]";
403
+
404
+ export const metaRowClass =
405
+ "flex flex-wrap items-center gap-1.5 text-[13px] text-[var(--ink-soft)]";
406
+
407
+ export const mutedDotClass =
408
+ "inline-block size-1 shrink-0 rounded-full bg-[var(--ink-soft)]";
409
+
410
+ export const metaStackClass = "flex flex-col items-end gap-1";
411
+
412
+ export const pillClass =
413
+ "inline-flex items-center rounded-full px-2 py-0.5 text-[12px] font-semibold";
414
+
415
+ export const pillSoftClass = "bg-[var(--accent-soft)] text-[var(--accent)]";
416
+
417
+ export const pillAlertClass = "bg-[var(--alert-soft)] text-[var(--alert)]";
418
+
419
+ export const timestampClass = "text-[13px] text-[var(--ink-soft)]";
420
+
421
+ export const bodyCopyClass = feedRowTextClass;
422
+
423
+ export const inboxTitleClass = "text-[16px] font-bold text-[var(--ink)]";
424
+
425
+ export const metricRowClass =
426
+ "flex flex-wrap items-center gap-3 text-[13px] text-[var(--ink-soft)]";
427
+
428
+ export const actionRowClass = "flex items-center gap-2";
429
+
430
+ export const feedPageClass = "flex w-full min-w-0 flex-col";
431
+
432
+ export const timelineLaneClass = feedClass;
433
+
434
+ export const inboxLaneClass = feedClass;
435
+
436
+ export const dmPageClass = "flex w-full min-w-0 flex-col";
437
+
438
+ export const stackGridClass = "flex flex-col";
439
+
440
+ /* Embedded tweet legacy aliases. */
441
+ export const embeddedTweetCardClass = embeddedCardClass;
442
+
443
+ export const embeddedTweetLabelClass = embeddedCardLabelClass;
444
+
445
+ export const embeddedTweetHeaderClass = embeddedCardHeaderClass;
446
+
447
+ export const embeddedTweetAuthorClass = "flex flex-wrap items-center gap-1.5";
448
+
449
+ export const embeddedTweetCopyClass = embeddedCardCopyClass;
450
+
451
+ /* DM legacy aliases (still used by DmWorkspace until rewrite). */
452
+ export const dmGridClass = dmShellClass;
453
+
454
+ export const dmListItemNonActive = "";
455
+
456
+ export const dmListCopyClass = dmListBodyClass;
457
+
458
+ export const dmPreviewTextClass = dmListPreviewClass;
459
+
460
+ export const threadShellClass = dmThreadClass;
461
+
462
+ export const threadHeaderClass = dmThreadHeaderClass;
463
+
464
+ export const threadTitleClass = dmThreadNameClass;
465
+
466
+ export const threadSubtitleClass = dmThreadSubtitleClass;
467
+
468
+ export const threadBioClass = "text-[14px] text-[var(--ink-soft)]";
469
+
470
+ export const threadDetailColumnClass =
471
+ "flex flex-col gap-3 rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)] p-4";
472
+
473
+ export const threadDetailHeaderClass = "flex items-center gap-3";
474
+
475
+ export const messageStackClass = dmMessagesClass;
476
+
477
+ export const messageRowClass = dmMessageRowClass;
478
+
479
+ export const messageRowOutboundClass = dmMessageRowOutboundClass;
480
+
481
+ export const messageMetaClass = dmMessageMetaClass;
482
+
483
+ export const messageBubbleClass = dmMessageBubbleClass;
484
+
485
+ export const messageBubbleOutboundClass = dmMessageBubbleOutboundClass;
486
+
487
+ export const contextHandleClass = "text-[14px] text-[var(--ink-soft)]";
488
+
489
+ export const contextBioClass = "text-[14px] leading-[1.4] text-[var(--ink)]";
490
+
491
+ export const contextStatsClass = "flex flex-col gap-0";
492
+
493
+ export const contextStatRowClass =
494
+ "flex items-baseline justify-between border-t border-[var(--line)] py-2 first:border-t-0 first:pt-0";
495
+
496
+ export const contextStatTermClass = "text-[13px] text-[var(--ink-soft)]";
497
+
498
+ export const contextStatValueClass = "text-[14px] font-bold text-[var(--ink)]";