@jant/core 0.3.21 → 0.3.22

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 (82) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/index.js +8 -0
  3. package/dist/lib/feed.js +112 -0
  4. package/dist/lib/navigation.js +9 -9
  5. package/dist/lib/render.js +48 -0
  6. package/dist/lib/theme-components.js +18 -18
  7. package/dist/lib/view.js +228 -0
  8. package/dist/routes/api/timeline.js +20 -16
  9. package/dist/routes/feed/rss.js +34 -78
  10. package/dist/routes/feed/sitemap.js +11 -26
  11. package/dist/routes/pages/archive.js +18 -195
  12. package/dist/routes/pages/collection.js +16 -70
  13. package/dist/routes/pages/home.js +25 -47
  14. package/dist/routes/pages/page.js +15 -27
  15. package/dist/routes/pages/post.js +25 -79
  16. package/dist/routes/pages/search.js +20 -130
  17. package/dist/theme/components/MediaGallery.js +10 -10
  18. package/dist/theme/components/index.js +1 -1
  19. package/dist/theme/components/timeline/ArticleCard.js +7 -11
  20. package/dist/theme/components/timeline/ImageCard.js +10 -13
  21. package/dist/theme/components/timeline/LinkCard.js +4 -7
  22. package/dist/theme/components/timeline/NoteCard.js +5 -8
  23. package/dist/theme/components/timeline/QuoteCard.js +3 -6
  24. package/dist/theme/components/timeline/ThreadPreview.js +9 -10
  25. package/dist/theme/components/timeline/TimelineFeed.js +8 -5
  26. package/dist/theme/components/timeline/TimelineItem.js +22 -2
  27. package/dist/theme/components/timeline/index.js +1 -1
  28. package/dist/theme/index.js +6 -3
  29. package/dist/theme/layouts/SiteLayout.js +10 -39
  30. package/dist/theme/pages/ArchivePage.js +157 -0
  31. package/dist/theme/pages/CollectionPage.js +63 -0
  32. package/dist/theme/pages/HomePage.js +26 -0
  33. package/dist/theme/pages/PostPage.js +48 -0
  34. package/dist/theme/pages/SearchPage.js +120 -0
  35. package/dist/theme/pages/SinglePage.js +23 -0
  36. package/dist/theme/pages/index.js +11 -0
  37. package/package.json +2 -1
  38. package/src/app.tsx +1 -1
  39. package/src/i18n/locales/en.po +31 -31
  40. package/src/i18n/locales/zh-Hans.po +31 -31
  41. package/src/i18n/locales/zh-Hant.po +31 -31
  42. package/src/index.ts +51 -2
  43. package/src/lib/__tests__/theme-components.test.ts +33 -14
  44. package/src/lib/__tests__/view.test.ts +375 -0
  45. package/src/lib/feed.ts +148 -0
  46. package/src/lib/navigation.ts +11 -11
  47. package/src/lib/render.tsx +67 -0
  48. package/src/lib/theme-components.ts +27 -35
  49. package/src/lib/view.ts +318 -0
  50. package/src/routes/api/__tests__/timeline.test.ts +3 -3
  51. package/src/routes/api/timeline.tsx +32 -25
  52. package/src/routes/feed/rss.ts +47 -94
  53. package/src/routes/feed/sitemap.ts +8 -30
  54. package/src/routes/pages/archive.tsx +24 -209
  55. package/src/routes/pages/collection.tsx +19 -75
  56. package/src/routes/pages/home.tsx +42 -76
  57. package/src/routes/pages/page.tsx +17 -28
  58. package/src/routes/pages/post.tsx +28 -86
  59. package/src/routes/pages/search.tsx +29 -151
  60. package/src/services/search.ts +2 -8
  61. package/src/theme/components/MediaGallery.tsx +12 -12
  62. package/src/theme/components/index.ts +1 -0
  63. package/src/theme/components/timeline/ArticleCard.tsx +7 -19
  64. package/src/theme/components/timeline/ImageCard.tsx +10 -20
  65. package/src/theme/components/timeline/LinkCard.tsx +4 -11
  66. package/src/theme/components/timeline/NoteCard.tsx +5 -12
  67. package/src/theme/components/timeline/QuoteCard.tsx +3 -10
  68. package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
  69. package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
  70. package/src/theme/components/timeline/TimelineItem.tsx +43 -4
  71. package/src/theme/components/timeline/index.ts +1 -1
  72. package/src/theme/index.ts +7 -3
  73. package/src/theme/layouts/SiteLayout.tsx +25 -77
  74. package/src/theme/layouts/index.ts +2 -1
  75. package/src/theme/pages/ArchivePage.tsx +160 -0
  76. package/src/theme/pages/CollectionPage.tsx +60 -0
  77. package/src/theme/pages/HomePage.tsx +42 -0
  78. package/src/theme/pages/PostPage.tsx +44 -0
  79. package/src/theme/pages/SearchPage.tsx +128 -0
  80. package/src/theme/pages/SinglePage.tsx +24 -0
  81. package/src/theme/pages/index.ts +13 -0
  82. package/src/types.ts +262 -38
@@ -6,17 +6,8 @@
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
9
 
12
10
  export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
- const excerpt = post.content
15
- ? post.content.length > 160
16
- ? post.content.slice(0, 160) + "..."
17
- : post.content
18
- : null;
19
-
20
11
  return (
21
12
  <article
22
13
  class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
@@ -25,28 +16,25 @@ export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
25
16
  <h2
26
17
  class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"} mb-1`}
27
18
  >
28
- <a href={permalink} class="u-url hover:underline">
19
+ <a href={post.permalink} class="u-url hover:underline">
29
20
  {post.title}
30
21
  </a>
31
22
  </h2>
32
23
  )}
33
- {!compact && excerpt && (
24
+ {!compact && post.excerpt && (
34
25
  <p class="e-content text-sm text-muted-foreground line-clamp-3">
35
- {excerpt}
26
+ {post.excerpt}
36
27
  </p>
37
28
  )}
38
29
  <footer class="mt-2 text-xs text-muted-foreground">
39
- <a href={permalink} class="u-url hover:underline">
40
- <time
41
- class="dt-published"
42
- datetime={time.toISOString(post.publishedAt)}
43
- >
44
- {time.formatDate(post.publishedAt)}
30
+ <a href={post.permalink} class="u-url hover:underline">
31
+ <time class="dt-published" datetime={post.publishedAt}>
32
+ {post.publishedAtFormatted}
45
33
  </time>
46
34
  </a>
47
35
  {!compact && (
48
36
  <span class="ml-2">
49
- <a href={permalink} class="hover:underline">
37
+ <a href={post.permalink} class="hover:underline">
50
38
  Read more &rarr;
51
39
  </a>
52
40
  </span>
@@ -7,18 +7,14 @@
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
9
  import { MediaGallery } from "../MediaGallery.js";
10
- import * as sqid from "../../../lib/sqid.js";
11
- import * as time from "../../../lib/time.js";
12
10
 
13
11
  export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
14
- const permalink = `/p/${sqid.encode(post.id)}`;
15
-
16
12
  if (compact) {
17
13
  return (
18
14
  <article class="h-entry timeline-card timeline-card-compact">
19
15
  {post.title && (
20
16
  <h2 class="p-name text-sm font-medium mb-1">
21
- <a href={permalink} class="u-url hover:underline">
17
+ <a href={post.permalink} class="u-url hover:underline">
22
18
  {post.title}
23
19
  </a>
24
20
  </h2>
@@ -30,12 +26,9 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
30
26
  />
31
27
  )}
32
28
  <footer class="mt-1 text-xs text-muted-foreground">
33
- <a href={permalink} class="u-url hover:underline">
34
- <time
35
- class="dt-published"
36
- datetime={time.toISOString(post.publishedAt)}
37
- >
38
- {time.formatDate(post.publishedAt)}
29
+ <a href={post.permalink} class="u-url hover:underline">
30
+ <time class="dt-published" datetime={post.publishedAt}>
31
+ {post.publishedAtFormatted}
39
32
  </time>
40
33
  </a>
41
34
  </footer>
@@ -45,15 +38,15 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
45
38
 
46
39
  return (
47
40
  <article class="h-entry timeline-card timeline-card-image">
48
- {post.mediaAttachments.length > 0 && (
41
+ {post.media.length > 0 && (
49
42
  <div class="timeline-card-image-gallery">
50
- <MediaGallery attachments={post.mediaAttachments} />
43
+ <MediaGallery attachments={post.media} />
51
44
  </div>
52
45
  )}
53
46
  <div class="p-4">
54
47
  {post.title && (
55
48
  <h2 class="p-name font-medium mb-1">
56
- <a href={permalink} class="u-url hover:underline">
49
+ <a href={post.permalink} class="u-url hover:underline">
57
50
  {post.title}
58
51
  </a>
59
52
  </h2>
@@ -65,12 +58,9 @@ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
65
58
  />
66
59
  )}
67
60
  <footer class="mt-2 text-xs text-muted-foreground">
68
- <a href={permalink} class="u-url hover:underline">
69
- <time
70
- class="dt-published"
71
- datetime={time.toISOString(post.publishedAt)}
72
- >
73
- {time.formatDate(post.publishedAt)}
61
+ <a href={post.permalink} class="u-url hover:underline">
62
+ <time class="dt-published" datetime={post.publishedAt}>
63
+ {post.publishedAtFormatted}
74
64
  </time>
75
65
  </a>
76
66
  </footer>
@@ -6,12 +6,8 @@
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
9
 
12
10
  export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
-
15
11
  return (
16
12
  <article
17
13
  class={`h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`}
@@ -36,7 +32,7 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
36
32
  class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
37
33
  >
38
34
  <a
39
- href={post.sourceUrl || permalink}
35
+ href={post.sourceUrl || post.permalink}
40
36
  class="u-url hover:underline"
41
37
  target={post.sourceUrl ? "_blank" : undefined}
42
38
  rel={post.sourceUrl ? "noopener noreferrer" : undefined}
@@ -52,12 +48,9 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
52
48
  />
53
49
  )}
54
50
  <footer class="mt-2 text-xs text-muted-foreground">
55
- <a href={permalink} class="hover:underline">
56
- <time
57
- class="dt-published"
58
- datetime={time.toISOString(post.publishedAt)}
59
- >
60
- {time.formatDate(post.publishedAt)}
51
+ <a href={post.permalink} class="hover:underline">
52
+ <time class="dt-published" datetime={post.publishedAt}>
53
+ {post.publishedAtFormatted}
61
54
  </time>
62
55
  </a>
63
56
  </footer>
@@ -7,12 +7,8 @@
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
9
  import { MediaGallery } from "../MediaGallery.js";
10
- import * as sqid from "../../../lib/sqid.js";
11
- import * as time from "../../../lib/time.js";
12
10
 
13
11
  export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
14
- const permalink = `/p/${sqid.encode(post.id)}`;
15
-
16
12
  return (
17
13
  <article
18
14
  class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
@@ -23,16 +19,13 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
23
19
  dangerouslySetInnerHTML={{ __html: post.contentHtml }}
24
20
  />
25
21
  )}
26
- {!compact && post.mediaAttachments.length > 0 && (
27
- <MediaGallery attachments={post.mediaAttachments} />
22
+ {!compact && post.media.length > 0 && (
23
+ <MediaGallery attachments={post.media} />
28
24
  )}
29
25
  <footer class="mt-2 text-xs text-muted-foreground">
30
- <a href={permalink} class="u-url hover:underline">
31
- <time
32
- class="dt-published"
33
- datetime={time.toISOString(post.publishedAt)}
34
- >
35
- {time.formatDate(post.publishedAt)}
26
+ <a href={post.permalink} class="u-url hover:underline">
27
+ <time class="dt-published" datetime={post.publishedAt}>
28
+ {post.publishedAtFormatted}
36
29
  </time>
37
30
  </a>
38
31
  </footer>
@@ -6,12 +6,8 @@
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
- import * as sqid from "../../../lib/sqid.js";
10
- import * as time from "../../../lib/time.js";
11
9
 
12
10
  export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
13
- const permalink = `/p/${sqid.encode(post.id)}`;
14
-
15
11
  return (
16
12
  <article
17
13
  class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
@@ -41,12 +37,9 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
41
37
  </div>
42
38
  )}
43
39
  <footer class="mt-2 text-xs text-muted-foreground">
44
- <a href={permalink} class="u-url hover:underline">
45
- <time
46
- class="dt-published"
47
- datetime={time.toISOString(post.publishedAt)}
48
- >
49
- {time.formatDate(post.publishedAt)}
40
+ <a href={post.permalink} class="u-url hover:underline">
41
+ <time class="dt-published" datetime={post.publishedAt}>
42
+ {post.publishedAtFormatted}
50
43
  </time>
51
44
  </a>
52
45
  </footer>
@@ -8,31 +8,31 @@ import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
9
  import type { ThreadPreviewProps } from "../../../types.js";
10
10
  import { TimelineItem } from "./TimelineItem.js";
11
- import * as sqid from "../../../lib/sqid.js";
11
+ import { TimelineItemFromPost } from "./TimelineItem.js";
12
12
 
13
13
  export const ThreadPreview: FC<ThreadPreviewProps> = ({
14
14
  rootPost,
15
15
  previewReplies,
16
16
  totalReplyCount,
17
+ theme,
17
18
  }) => {
18
19
  const { t } = useLingui();
19
- const permalink = `/p/${sqid.encode(rootPost.id)}`;
20
20
  const remainingCount = totalReplyCount - previewReplies.length;
21
21
 
22
22
  return (
23
23
  <div class="timeline-thread">
24
- <TimelineItem item={{ post: rootPost }} />
24
+ <TimelineItem item={{ post: rootPost }} theme={theme} />
25
25
  {previewReplies.length > 0 && (
26
26
  <div class="timeline-thread-replies">
27
27
  {previewReplies.map((reply) => (
28
28
  <div key={reply.id} class="timeline-thread-reply">
29
- <TimelineItem item={{ post: reply }} compact />
29
+ <TimelineItemFromPost post={reply} compact theme={theme} />
30
30
  </div>
31
31
  ))}
32
32
  {remainingCount > 0 && (
33
33
  <div class="timeline-thread-reply">
34
34
  <a
35
- href={permalink}
35
+ href={rootPost.permalink}
36
36
  class="text-sm text-muted-foreground hover:text-foreground hover:underline"
37
37
  >
38
38
  {t({
@@ -8,30 +8,34 @@ import type { FC } from "hono/jsx";
8
8
  import { useLingui } from "@lingui/react/macro";
9
9
  import type { TimelineFeedProps } from "../../../types.js";
10
10
  import { TimelineItem } from "./TimelineItem.js";
11
- import { ThreadPreview } from "./ThreadPreview.js";
11
+ import { ThreadPreview as DefaultThreadPreview } from "./ThreadPreview.js";
12
12
 
13
13
  export const TimelineFeed: FC<TimelineFeedProps> = ({
14
14
  items,
15
15
  hasMore,
16
16
  nextCursor,
17
+ theme,
17
18
  }) => {
18
19
  const { t } = useLingui();
19
20
 
21
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
22
+
20
23
  return (
21
24
  <div>
22
25
  <div id="timeline-feed" class="flex flex-col gap-4">
23
26
  {items.map((item) => {
24
27
  if (item.threadPreview) {
25
28
  return (
26
- <ThreadPreview
29
+ <ResolvedThreadPreview
27
30
  key={item.post.id}
28
31
  rootPost={item.post}
29
32
  previewReplies={item.threadPreview.replies}
30
33
  totalReplyCount={item.threadPreview.totalReplyCount}
34
+ theme={theme}
31
35
  />
32
36
  );
33
37
  }
34
- return <TimelineItem key={item.post.id} item={item} />;
38
+ return <TimelineItem key={item.post.id} item={item} theme={theme} />;
35
39
  })}
36
40
  </div>
37
41
  {hasMore && nextCursor && (
@@ -2,10 +2,16 @@
2
2
  * Timeline Item Component
3
3
  *
4
4
  * Dispatches to the correct card component based on post type.
5
+ * Resolves card overrides from theme components if provided.
5
6
  */
6
7
 
7
8
  import type { FC } from "hono/jsx";
8
- import type { TimelineItemData, TimelineCardProps } from "../../../types.js";
9
+ import type {
10
+ TimelineItemView,
11
+ TimelineCardProps,
12
+ ThemeComponents,
13
+ PostView,
14
+ } from "../../../types.js";
9
15
  import { NoteCard } from "./NoteCard.js";
10
16
  import { ArticleCard } from "./ArticleCard.js";
11
17
  import { LinkCard } from "./LinkCard.js";
@@ -22,18 +28,51 @@ const CARD_MAP: Record<PostType, FC<TimelineCardProps>> = {
22
28
  page: NoteCard,
23
29
  };
24
30
 
31
+ const THEME_KEY_MAP: Record<PostType, keyof ThemeComponents> = {
32
+ note: "NoteCard",
33
+ article: "ArticleCard",
34
+ link: "LinkCard",
35
+ quote: "QuoteCard",
36
+ image: "ImageCard",
37
+ page: "NoteCard",
38
+ };
39
+
25
40
  interface TimelineItemProps {
26
- item: TimelineItemData;
41
+ item: TimelineItemView;
42
+ compact?: boolean;
43
+ /** Override card component (for direct overrides) */
44
+ cardOverride?: FC<TimelineCardProps>;
45
+ /** Theme components for cascade resolution */
46
+ theme?: ThemeComponents;
47
+ }
48
+
49
+ interface TimelineItemFromPostProps {
50
+ post: PostView;
27
51
  compact?: boolean;
28
- /** Override card component (for theme overrides) */
29
52
  cardOverride?: FC<TimelineCardProps>;
53
+ theme?: ThemeComponents;
30
54
  }
31
55
 
32
56
  export const TimelineItem: FC<TimelineItemProps> = ({
33
57
  item,
34
58
  compact,
35
59
  cardOverride,
60
+ theme,
36
61
  }) => {
37
- const Card = cardOverride ?? CARD_MAP[item.post.type];
62
+ const themeKey = THEME_KEY_MAP[item.post.type];
63
+ const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
64
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[item.post.type];
38
65
  return <Card post={item.post} compact={compact} />;
39
66
  };
67
+
68
+ export const TimelineItemFromPost: FC<TimelineItemFromPostProps> = ({
69
+ post,
70
+ compact,
71
+ cardOverride,
72
+ theme,
73
+ }) => {
74
+ const themeKey = THEME_KEY_MAP[post.type];
75
+ const themeCard = theme?.[themeKey] as FC<TimelineCardProps> | undefined;
76
+ const Card = cardOverride ?? themeCard ?? CARD_MAP[post.type];
77
+ return <Card post={post} compact={compact} />;
78
+ };
@@ -4,5 +4,5 @@ export { LinkCard } from "./LinkCard.js";
4
4
  export { QuoteCard } from "./QuoteCard.js";
5
5
  export { ImageCard } from "./ImageCard.js";
6
6
  export { ThreadPreview } from "./ThreadPreview.js";
7
- export { TimelineItem } from "./TimelineItem.js";
7
+ export { TimelineItem, TimelineItemFromPost } from "./TimelineItem.js";
8
8
  export { TimelineFeed } from "./TimelineFeed.js";
@@ -5,12 +5,13 @@
5
5
  *
6
6
  * @example
7
7
  * ```typescript
8
- * import { PostCard } from "@jant/core/theme";
8
+ * import { PostPage } from "@jant/core/theme";
9
+ * import type { PostPageProps } from "@jant/core";
9
10
  *
10
- * export function MyPostCard(props) {
11
+ * export function MyPostPage(props: PostPageProps) {
11
12
  * return (
12
13
  * <div class="my-wrapper">
13
- * <PostCard {...props} />
14
+ * <PostPage {...props} />
14
15
  * </div>
15
16
  * );
16
17
  * }
@@ -22,3 +23,6 @@ export * from "./layouts/index.js";
22
23
 
23
24
  // UI components
24
25
  export * from "./components/index.js";
26
+
27
+ // Page components
28
+ export * from "./pages/index.js";
@@ -6,87 +6,41 @@
6
6
  */
7
7
 
8
8
  import type { FC, PropsWithChildren } from "hono/jsx";
9
- import type { NavigationLink } from "../../types.js";
10
-
11
- export interface SiteLayoutProps {
12
- siteName: string;
13
- navigationLinks: NavigationLink[];
14
- currentPath: string;
15
- }
16
-
17
- /**
18
- * Determine if a navigation link is active based on the current path.
19
- *
20
- * @param linkUrl - The link's URL
21
- * @param currentPath - The current page path
22
- * @returns Whether the link should be shown as active
23
- */
24
- function isLinkActive(linkUrl: string, currentPath: string): boolean {
25
- // External links are never active
26
- if (linkUrl.startsWith("http://") || linkUrl.startsWith("https://")) {
27
- return false;
28
- }
29
-
30
- // Exact match for home
31
- if (linkUrl === "/") {
32
- return currentPath === "/";
33
- }
34
-
35
- // Prefix match for other internal links
36
- return currentPath === linkUrl || currentPath.startsWith(linkUrl + "/");
37
- }
38
-
39
- /**
40
- * Check if a URL is external
41
- */
42
- function isExternalUrl(url: string): boolean {
43
- return url.startsWith("http://") || url.startsWith("https://");
44
- }
9
+ import type { NavLinkView, SiteLayoutProps } from "../../types.js";
45
10
 
46
11
  /**
47
12
  * Render navigation links with dot indicator for active state.
48
13
  */
49
- function NavLinks({
50
- navigationLinks,
51
- currentPath,
52
- }: {
53
- navigationLinks: NavigationLink[];
54
- currentPath: string;
55
- }) {
14
+ function NavLinks({ links }: { links: NavLinkView[] }) {
56
15
  return (
57
16
  <>
58
- {navigationLinks.map((link) => {
59
- const active = isLinkActive(link.url, currentPath);
60
- const external = isExternalUrl(link.url);
61
- return (
62
- <a
63
- key={link.id}
64
- href={link.url}
65
- class={`text-sm flex items-center gap-2 py-0.5 ${
66
- active
67
- ? "text-primary font-medium"
68
- : "text-muted-foreground hover:text-foreground"
69
- }`}
70
- {...(external
71
- ? { target: "_blank", rel: "noopener noreferrer" }
72
- : {})}
73
- >
74
- <span
75
- class={`size-1.5 rounded-full shrink-0 ${active ? "bg-primary" : "bg-transparent"}`}
76
- />
77
- {link.label}
78
- {external && <span class="ml-1 text-xs opacity-50">↗</span>}
79
- </a>
80
- );
81
- })}
17
+ {links.map((link) => (
18
+ <a
19
+ key={link.id}
20
+ href={link.url}
21
+ class={`text-sm flex items-center gap-2 py-0.5 ${
22
+ link.isActive
23
+ ? "text-primary font-medium"
24
+ : "text-muted-foreground hover:text-foreground"
25
+ }`}
26
+ {...(link.isExternal
27
+ ? { target: "_blank", rel: "noopener noreferrer" }
28
+ : {})}
29
+ >
30
+ <span
31
+ class={`size-1.5 rounded-full shrink-0 ${link.isActive ? "bg-primary" : "bg-transparent"}`}
32
+ />
33
+ {link.label}
34
+ {link.isExternal && <span class="ml-1 text-xs opacity-50">↗</span>}
35
+ </a>
36
+ ))}
82
37
  </>
83
38
  );
84
39
  }
85
40
 
86
41
  export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
87
42
  siteName,
88
- navigationLinks,
89
- currentPath,
43
+ links,
90
44
  children,
91
45
  }) => {
92
46
  return (
@@ -157,10 +111,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
157
111
  </button>
158
112
  </div>
159
113
  <nav class="flex flex-col gap-0.5">
160
- <NavLinks
161
- navigationLinks={navigationLinks}
162
- currentPath={currentPath}
163
- />
114
+ <NavLinks links={links} />
164
115
  </nav>
165
116
  </aside>
166
117
 
@@ -170,10 +121,7 @@ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
170
121
  {siteName}
171
122
  </a>
172
123
  <nav class="flex flex-col gap-0.5">
173
- <NavLinks
174
- navigationLinks={navigationLinks}
175
- currentPath={currentPath}
176
- />
124
+ <NavLinks links={links} />
177
125
  </nav>
178
126
  </aside>
179
127
 
@@ -4,4 +4,5 @@ export {
4
4
  type ToastProps,
5
5
  } from "./BaseLayout.js";
6
6
  export { DashLayout, type DashLayoutProps } from "./DashLayout.js";
7
- export { SiteLayout, type SiteLayoutProps } from "./SiteLayout.js";
7
+ export { SiteLayout } from "./SiteLayout.js";
8
+ export type { SiteLayoutProps } from "../../types.js";