@jant/core 0.3.23 → 0.3.24

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 (169) hide show
  1. package/dist/app.js +4 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +61 -72
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
package/src/lib/view.ts CHANGED
@@ -1,33 +1,44 @@
1
1
  /**
2
- * View Model Conversions
2
+ * View Model Conversions (v2)
3
3
  *
4
4
  * Transforms raw database models into render-ready View types.
5
- * Theme components receive only View types no lib/ imports needed.
5
+ * Theme components receive only View types -- no lib/ imports needed.
6
6
  */
7
7
 
8
8
  import type { Context } from "hono";
9
9
  import type {
10
10
  Post,
11
11
  PostWithMedia,
12
+ Page,
12
13
  Media,
13
14
  MediaView,
14
15
  PostView,
15
- NavLinkView,
16
- NavigationLink,
16
+ PageView,
17
+ NavItemView,
18
+ NavItem,
17
19
  SearchResult,
18
20
  SearchResultView,
19
21
  ArchiveGroup,
22
+ Format,
23
+ Status,
24
+ NavItemType,
20
25
  } from "../types.js";
21
26
  import { encode } from "./sqid.js";
22
- import { toISOString, formatDate } from "./time.js";
27
+ import {
28
+ toISOString,
29
+ formatDate,
30
+ formatTime,
31
+ formatRelativeTime,
32
+ } from "./time.js";
23
33
  import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
34
+ import { getHtmlExcerpt } from "./excerpt.js";
24
35
 
25
36
  // =============================================================================
26
37
  // Media Context
27
38
  // =============================================================================
28
39
 
29
40
  /**
30
- * Central media config extracted once per request from env.
41
+ * Central media config -- extracted once per request from env.
31
42
  */
32
43
  export interface MediaContext {
33
44
  r2PublicUrl?: string;
@@ -40,12 +51,6 @@ export interface MediaContext {
40
51
  *
41
52
  * @param c - Hono context
42
53
  * @returns MediaContext with env values
43
- *
44
- * @example
45
- * ```ts
46
- * const mediaCtx = createMediaContext(c);
47
- * const postView = toPostView(post, mediaCtx);
48
- * ```
49
54
  */
50
55
  export function createMediaContext(c: Context): MediaContext {
51
56
  return {
@@ -100,25 +105,26 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
100
105
  * Converts a PostWithMedia to a render-ready PostView.
101
106
  *
102
107
  * @param post - Post with media attachments from database
103
- * @param ctx - Media context with URL configuration
108
+ * @param _ctx - Media context with URL configuration
104
109
  * @returns Render-ready PostView with pre-computed fields
105
- *
106
- * @example
107
- * ```ts
108
- * const mediaCtx = createMediaContext(c);
109
- * const postView = toPostView({ ...post, mediaAttachments: [...] }, mediaCtx);
110
- * ```
111
110
  */
112
111
  export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
113
- const permalink = `/p/${encode(post.id)}`;
112
+ const permalink = post.slug ? `/${post.slug}` : `/p/${encode(post.id)}`;
114
113
 
115
- // Pre-compute excerpt from raw content
114
+ // Pre-compute excerpt from raw body
116
115
  let excerpt: string | undefined;
117
- if (post.content) {
116
+ if (post.body) {
118
117
  excerpt =
119
- post.content.length > 160
120
- ? post.content.slice(0, 160) + "..."
121
- : post.content;
118
+ post.body.length > 160 ? post.body.slice(0, 160) + "..." : post.body;
119
+ }
120
+
121
+ // Pre-compute HTML summary for article-style posts (with title)
122
+ let summaryHtml: string | undefined;
123
+ let summaryHasMore: boolean | undefined;
124
+ if (post.title && post.bodyHtml) {
125
+ const result = getHtmlExcerpt(post.bodyHtml);
126
+ summaryHtml = result.excerpt;
127
+ summaryHasMore = result.hasMore;
122
128
  }
123
129
 
124
130
  // Convert media attachments
@@ -135,31 +141,34 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
135
141
  return {
136
142
  id: post.id,
137
143
  permalink,
144
+ slug: post.slug ?? undefined,
138
145
  title: post.title ?? undefined,
139
- contentHtml: post.contentHtml ?? undefined,
146
+ bodyHtml: post.bodyHtml ?? undefined,
140
147
  excerpt,
141
- type: post.type,
142
- visibility: post.visibility,
143
- path: post.path ?? undefined,
148
+ summaryHtml,
149
+ summaryHasMore,
150
+ url: post.url ?? undefined,
151
+ quoteText: post.quoteText ?? undefined,
152
+ format: post.format as Format,
153
+ status: post.status as Status,
154
+ featured: post.featured === 1,
155
+ pinned: post.pinned === 1,
156
+ rating: post.rating ?? undefined,
157
+ collectionId: post.collectionId ?? undefined,
144
158
  publishedAt: toISOString(post.publishedAt),
145
159
  publishedAtFormatted: formatDate(post.publishedAt),
160
+ publishedAtTime: formatTime(post.publishedAt),
161
+ publishedAtRelative: formatRelativeTime(post.publishedAt),
146
162
  updatedAt: toISOString(post.updatedAt),
147
- sourceUrl: post.sourceUrl ?? undefined,
148
- sourceName: post.sourceName ?? undefined,
149
- sourceDomain: post.sourceDomain ?? undefined,
150
163
  media,
151
164
  replyToId: post.replyToId ?? undefined,
152
165
  threadRootId: post.threadId ?? undefined,
153
- content: post.content ?? undefined,
166
+ body: post.body ?? undefined,
154
167
  };
155
168
  }
156
169
 
157
170
  /**
158
171
  * Batch converts PostWithMedia[] to PostView[].
159
- *
160
- * @param posts - Array of posts with media
161
- * @param ctx - Media context
162
- * @returns Array of PostView
163
172
  */
164
173
  export function toPostViews(
165
174
  posts: PostWithMedia[],
@@ -170,10 +179,6 @@ export function toPostViews(
170
179
 
171
180
  /**
172
181
  * Converts a bare Post (no media) to a PostView with empty media array.
173
- *
174
- * @param post - Post without media
175
- * @param ctx - Media context (unused but kept for consistency)
176
- * @returns PostView with empty media
177
182
  */
178
183
  export function toPostViewFromPost(post: Post, ctx: MediaContext): PostView {
179
184
  return toPostView({ ...post, mediaAttachments: [] }, ctx);
@@ -181,10 +186,6 @@ export function toPostViewFromPost(post: Post, ctx: MediaContext): PostView {
181
186
 
182
187
  /**
183
188
  * Batch converts Post[] (no media) to PostView[].
184
- *
185
- * @param posts - Array of posts without media
186
- * @param ctx - Media context
187
- * @returns Array of PostView
188
189
  */
189
190
  export function toPostViewsFromPosts(
190
191
  posts: Post[],
@@ -193,55 +194,65 @@ export function toPostViewsFromPosts(
193
194
  return posts.map((p) => toPostViewFromPost(p, ctx));
194
195
  }
195
196
 
197
+ // =============================================================================
198
+ // Page Conversions
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Converts a Page to a render-ready PageView.
203
+ */
204
+ export function toPageView(page: Page): PageView {
205
+ return {
206
+ id: page.id,
207
+ slug: page.slug,
208
+ title: page.title ?? undefined,
209
+ bodyHtml: page.bodyHtml ?? undefined,
210
+ status: page.status as Status,
211
+ createdAt: toISOString(page.createdAt),
212
+ updatedAt: toISOString(page.updatedAt),
213
+ };
214
+ }
215
+
196
216
  // =============================================================================
197
217
  // Navigation Conversions
198
218
  // =============================================================================
199
219
 
200
220
  /**
201
- * Converts a NavigationLink to a NavLinkView with pre-computed state.
202
- *
203
- * @param link - Raw navigation link from database
204
- * @param currentPath - Current page path for active state computation
205
- * @returns NavLinkView with isActive and isExternal pre-computed
221
+ * Converts a NavItem to a NavItemView with pre-computed state.
206
222
  */
207
- export function toNavLinkView(
208
- link: NavigationLink,
209
- currentPath: string,
210
- ): NavLinkView {
223
+ export function toNavItemView(item: NavItem, currentPath: string): NavItemView {
211
224
  const isExternal =
212
- link.url.startsWith("http://") || link.url.startsWith("https://");
225
+ item.url.startsWith("http://") || item.url.startsWith("https://");
213
226
 
214
227
  let isActive = false;
215
228
  if (!isExternal) {
216
- if (link.url === "/") {
229
+ if (item.url === "/") {
217
230
  isActive = currentPath === "/";
218
231
  } else {
219
232
  isActive =
220
- currentPath === link.url || currentPath.startsWith(link.url + "/");
233
+ currentPath === item.url || currentPath.startsWith(item.url + "/");
221
234
  }
222
235
  }
223
236
 
224
237
  return {
225
- id: link.id,
226
- label: link.label,
227
- url: link.url,
238
+ id: item.id,
239
+ type: item.type as NavItemType,
240
+ label: item.label,
241
+ url: item.url,
242
+ pageId: item.pageId ?? undefined,
228
243
  isActive,
229
244
  isExternal,
230
245
  };
231
246
  }
232
247
 
233
248
  /**
234
- * Batch converts NavigationLink[] to NavLinkView[].
235
- *
236
- * @param links - Raw navigation links
237
- * @param currentPath - Current page path
238
- * @returns Array of NavLinkView
249
+ * Batch converts NavItem[] to NavItemView[].
239
250
  */
240
- export function toNavLinkViews(
241
- links: NavigationLink[],
251
+ export function toNavItemViews(
252
+ items: NavItem[],
242
253
  currentPath: string,
243
- ): NavLinkView[] {
244
- return links.map((l) => toNavLinkView(l, currentPath));
254
+ ): NavItemView[] {
255
+ return items.map((item) => toNavItemView(item, currentPath));
245
256
  }
246
257
 
247
258
  // =============================================================================
@@ -250,10 +261,6 @@ export function toNavLinkViews(
250
261
 
251
262
  /**
252
263
  * Converts a SearchResult to a SearchResultView with PostView.
253
- *
254
- * @param result - Raw search result
255
- * @param ctx - Media context
256
- * @returns SearchResultView with PostView
257
264
  */
258
265
  export function toSearchResultView(
259
266
  result: SearchResult,
@@ -268,10 +275,6 @@ export function toSearchResultView(
268
275
 
269
276
  /**
270
277
  * Batch converts SearchResult[] to SearchResultView[].
271
- *
272
- * @param results - Raw search results
273
- * @param ctx - Media context
274
- * @returns Array of SearchResultView
275
278
  */
276
279
  export function toSearchResultViews(
277
280
  results: SearchResult[],
@@ -286,10 +289,6 @@ export function toSearchResultViews(
286
289
 
287
290
  /**
288
291
  * Converts a grouped post map to typed ArchiveGroup[].
289
- *
290
- * @param grouped - Map of "YYYY-MM" keys to Post arrays
291
- * @param ctx - Media context
292
- * @returns Array of ArchiveGroup with pre-formatted labels
293
292
  */
294
293
  export function toArchiveGroups(
295
294
  grouped: Map<string, Post[]>,
@@ -300,7 +299,6 @@ export function toArchiveGroups(
300
299
  const [year, month] = yearMonth.split("-");
301
300
  if (!year || !month) continue;
302
301
 
303
- // Format label like "February 2024"
304
302
  const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
305
303
  const label = date.toLocaleDateString("en-US", {
306
304
  year: "numeric",
package/src/preset.css CHANGED
@@ -8,7 +8,9 @@
8
8
  @source "./";
9
9
 
10
10
  @import "basecoat-css";
11
+ @plugin "@tailwindcss/typography";
11
12
  @import "./styles/components.css";
13
+ @import "./themes/threads/style.css";
12
14
 
13
15
  @theme {
14
16
  --radius-default: 0.5rem;
@@ -22,3 +24,46 @@
22
24
  .dark {
23
25
  --success: oklch(0.627 0.194 149.214);
24
26
  }
27
+
28
+ /**
29
+ * Typography (prose) — integrate with BaseCoat theme colors.
30
+ *
31
+ * The @tailwindcss/typography plugin ships with hardcoded slate values.
32
+ * We override them to use BaseCoat's CSS variables so that:
33
+ * - Light/dark mode switches automatically
34
+ * - Prose text matches the rest of the theme
35
+ * - Links are understated (same color as text, underline only)
36
+ *
37
+ * Design reference: Medium, Substack, NYT — body links inherit text
38
+ * color and rely on underline as the sole affordance.
39
+ */
40
+ .prose {
41
+ --tw-prose-body: inherit;
42
+ --tw-prose-headings: var(--color-foreground);
43
+ --tw-prose-lead: var(--color-muted-foreground);
44
+ --tw-prose-links: inherit;
45
+ --tw-prose-bold: inherit;
46
+ --tw-prose-counters: var(--color-muted-foreground);
47
+ --tw-prose-bullets: var(--color-muted-foreground);
48
+ --tw-prose-hr: var(--color-border);
49
+ --tw-prose-quotes: var(--color-foreground);
50
+ --tw-prose-quote-borders: var(--color-border);
51
+ --tw-prose-captions: var(--color-muted-foreground);
52
+ --tw-prose-kbd: var(--color-foreground);
53
+ --tw-prose-code: var(--color-foreground);
54
+ --tw-prose-pre-code: var(--color-muted-foreground);
55
+ --tw-prose-pre-bg: var(--color-muted);
56
+ --tw-prose-th-borders: var(--color-border);
57
+ --tw-prose-td-borders: var(--color-border);
58
+
59
+ /* Links: same color as surrounding text, underline only, no bold */
60
+ a {
61
+ font-weight: inherit;
62
+ text-underline-offset: 2px;
63
+ text-decoration-color: var(--color-border);
64
+ }
65
+
66
+ a:hover {
67
+ text-decoration-color: currentColor;
68
+ }
69
+ }