@jant/core 0.3.22 → 0.3.23

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 (78) hide show
  1. package/dist/app.js +22 -3
  2. package/dist/index.js +3 -4
  3. package/dist/lib/render.js +1 -1
  4. package/dist/lib/view.js +1 -1
  5. package/dist/routes/api/timeline.js +3 -3
  6. package/dist/routes/pages/archive.js +1 -1
  7. package/dist/routes/pages/collection.js +1 -1
  8. package/dist/routes/pages/home.js +1 -1
  9. package/dist/routes/pages/page.js +1 -1
  10. package/dist/routes/pages/post.js +1 -1
  11. package/dist/routes/pages/search.js +1 -1
  12. package/dist/theme/components/index.js +0 -2
  13. package/dist/theme/index.js +10 -16
  14. package/dist/theme/layouts/index.js +0 -1
  15. package/dist/themes/minimal/MinimalSiteLayout.js +83 -0
  16. package/dist/themes/minimal/index.js +65 -0
  17. package/dist/{theme → themes/minimal}/pages/ArchivePage.js +7 -8
  18. package/dist/{theme → themes/minimal}/pages/CollectionPage.js +7 -5
  19. package/dist/{theme → themes/minimal}/pages/HomePage.js +2 -3
  20. package/dist/{theme → themes/minimal}/pages/PostPage.js +5 -6
  21. package/dist/{theme → themes/minimal}/pages/SearchPage.js +11 -10
  22. package/dist/{theme → themes/minimal}/pages/SinglePage.js +3 -4
  23. package/dist/themes/minimal/timeline/ArticleCard.js +36 -0
  24. package/dist/themes/minimal/timeline/ImageCard.js +67 -0
  25. package/dist/{theme/components → themes/minimal}/timeline/LinkCard.js +14 -26
  26. package/dist/{theme/components → themes/minimal}/timeline/NoteCard.js +7 -7
  27. package/dist/{theme/components → themes/minimal}/timeline/QuoteCard.js +6 -6
  28. package/dist/{theme/components → themes/minimal}/timeline/ThreadPreview.js +13 -18
  29. package/dist/themes/minimal/timeline/TimelineFeed.js +48 -0
  30. package/dist/{theme/components → themes/minimal}/timeline/TimelineItem.js +1 -2
  31. package/package.json +1 -1
  32. package/src/app.tsx +26 -3
  33. package/src/i18n/locales/en.po +47 -47
  34. package/src/i18n/locales/zh-Hans.po +47 -47
  35. package/src/i18n/locales/zh-Hant.po +47 -47
  36. package/src/index.ts +4 -5
  37. package/src/lib/__tests__/view.test.ts +18 -16
  38. package/src/lib/render.tsx +1 -1
  39. package/src/lib/view.ts +1 -1
  40. package/src/routes/api/timeline.tsx +3 -3
  41. package/src/routes/pages/archive.tsx +1 -1
  42. package/src/routes/pages/collection.tsx +1 -1
  43. package/src/routes/pages/home.tsx +1 -1
  44. package/src/routes/pages/page.tsx +1 -1
  45. package/src/routes/pages/post.tsx +1 -1
  46. package/src/routes/pages/search.tsx +1 -1
  47. package/src/styles/components.css +0 -54
  48. package/src/theme/components/index.ts +0 -13
  49. package/src/theme/index.ts +10 -16
  50. package/src/theme/layouts/index.ts +0 -1
  51. package/src/themes/minimal/MinimalSiteLayout.tsx +100 -0
  52. package/src/themes/minimal/index.ts +83 -0
  53. package/src/{theme → themes/minimal}/pages/ArchivePage.tsx +8 -11
  54. package/src/{theme → themes/minimal}/pages/CollectionPage.tsx +6 -6
  55. package/src/{theme → themes/minimal}/pages/HomePage.tsx +3 -4
  56. package/src/{theme → themes/minimal}/pages/PostPage.tsx +6 -7
  57. package/src/{theme → themes/minimal}/pages/SearchPage.tsx +11 -17
  58. package/src/{theme → themes/minimal}/pages/SinglePage.tsx +4 -5
  59. package/src/themes/minimal/timeline/ArticleCard.tsx +37 -0
  60. package/src/themes/minimal/timeline/ImageCard.tsx +63 -0
  61. package/src/themes/minimal/timeline/LinkCard.tsx +48 -0
  62. package/src/{theme/components → themes/minimal}/timeline/NoteCard.tsx +10 -9
  63. package/src/{theme/components → themes/minimal}/timeline/QuoteCard.tsx +9 -8
  64. package/src/{theme/components → themes/minimal}/timeline/ThreadPreview.tsx +14 -16
  65. package/src/{theme/components → themes/minimal}/timeline/TimelineFeed.tsx +14 -13
  66. package/src/{theme/components → themes/minimal}/timeline/TimelineItem.tsx +1 -4
  67. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  68. package/dist/theme/components/timeline/ImageCard.js +0 -83
  69. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  70. package/dist/theme/components/timeline/index.js +0 -8
  71. package/dist/theme/layouts/SiteLayout.js +0 -131
  72. package/dist/theme/pages/index.js +0 -11
  73. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  74. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  75. package/src/theme/components/timeline/LinkCard.tsx +0 -59
  76. package/src/theme/components/timeline/index.ts +0 -8
  77. package/src/theme/layouts/SiteLayout.tsx +0 -132
  78. package/src/theme/pages/index.ts +0 -13
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Minimal Theme - Site Layout
3
+ *
4
+ * Single-column, centered layout with horizontal nav.
5
+ * Inspired by Tufte CSS and Manton.org.
6
+ */
7
+
8
+ import type { FC, PropsWithChildren } from "hono/jsx";
9
+ import type { NavLinkView, SiteLayoutProps } from "../../types.js";
10
+
11
+ function NavLinks({ links }: { links: NavLinkView[] }) {
12
+ return (
13
+ <>
14
+ {links.map((link) => (
15
+ <a
16
+ key={link.id}
17
+ href={link.url}
18
+ class={`text-sm ${
19
+ link.isActive
20
+ ? "text-foreground font-medium"
21
+ : "text-muted-foreground hover:text-foreground"
22
+ }`}
23
+ {...(link.isExternal
24
+ ? { target: "_blank", rel: "noopener noreferrer" }
25
+ : {})}
26
+ >
27
+ {link.label}
28
+ {link.isExternal && (
29
+ <span class="ml-0.5 text-xs opacity-50">&#8599;</span>
30
+ )}
31
+ </a>
32
+ ))}
33
+ </>
34
+ );
35
+ }
36
+
37
+ export const SiteLayout: FC<PropsWithChildren<SiteLayoutProps>> = ({
38
+ siteName,
39
+ links,
40
+ children,
41
+ }) => {
42
+ return (
43
+ <div
44
+ class="max-w-2xl mx-auto px-4 py-8"
45
+ data-signals={JSON.stringify({ _menuOpen: false })}
46
+ >
47
+ {/* Header */}
48
+ <header class="mb-12">
49
+ <div class="flex items-center justify-between">
50
+ <a href="/" class="text-xl font-semibold">
51
+ {siteName}
52
+ </a>
53
+
54
+ {/* Mobile menu toggle */}
55
+ {links.length > 0 && (
56
+ <button
57
+ data-on:click="$_menuOpen = !$_menuOpen"
58
+ class="p-2 -mr-2 text-muted-foreground hover:text-foreground sm:hidden"
59
+ aria-label="Toggle menu"
60
+ >
61
+ <svg
62
+ class="size-5"
63
+ fill="none"
64
+ viewBox="0 0 24 24"
65
+ stroke-width="1.5"
66
+ stroke="currentColor"
67
+ >
68
+ <path
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
72
+ />
73
+ </svg>
74
+ </button>
75
+ )}
76
+ </div>
77
+
78
+ {/* Desktop nav (inline) */}
79
+ {links.length > 0 && (
80
+ <nav class="hidden sm:flex flex-wrap gap-x-4 gap-y-1 mt-3">
81
+ <NavLinks links={links} />
82
+ </nav>
83
+ )}
84
+
85
+ {/* Mobile nav (collapsible) */}
86
+ {links.length > 0 && (
87
+ <nav
88
+ class="sm:hidden flex flex-col gap-1 mt-3 overflow-hidden"
89
+ data-show="$_menuOpen"
90
+ >
91
+ <NavLinks links={links} />
92
+ </nav>
93
+ )}
94
+ </header>
95
+
96
+ {/* Main content */}
97
+ <main>{children}</main>
98
+ </div>
99
+ );
100
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Minimal Theme
3
+ *
4
+ * A content-first, borderless theme inspired by Tufte CSS and Manton.org.
5
+ * Single-column layout with serif-friendly typography and generous whitespace.
6
+ *
7
+ * This is the default theme for Jant.
8
+ */
9
+
10
+ import type { JantTheme, ThemeComponents } from "../../types.js";
11
+ import type { ColorTheme } from "../../theme/color-themes.js";
12
+
13
+ // Layout
14
+ import { SiteLayout } from "./MinimalSiteLayout.js";
15
+
16
+ // Pages
17
+ import { HomePage } from "./pages/HomePage.js";
18
+ import { PostPage } from "./pages/PostPage.js";
19
+ import { SinglePage } from "./pages/SinglePage.js";
20
+ import { ArchivePage } from "./pages/ArchivePage.js";
21
+ import { SearchPage } from "./pages/SearchPage.js";
22
+ import { CollectionPage } from "./pages/CollectionPage.js";
23
+
24
+ // Timeline
25
+ import { NoteCard } from "./timeline/NoteCard.js";
26
+ import { ArticleCard } from "./timeline/ArticleCard.js";
27
+ import { LinkCard } from "./timeline/LinkCard.js";
28
+ import { QuoteCard } from "./timeline/QuoteCard.js";
29
+ import { ImageCard } from "./timeline/ImageCard.js";
30
+ import { ThreadPreview } from "./timeline/ThreadPreview.js";
31
+ import { TimelineFeed } from "./timeline/TimelineFeed.js";
32
+
33
+ export interface ThemeOptions {
34
+ /** Override individual components */
35
+ components?: Partial<ThemeComponents>;
36
+ /** CSS variable overrides */
37
+ cssVariables?: Record<string, string>;
38
+ /** Custom color themes */
39
+ colorThemes?: ColorTheme[];
40
+ }
41
+
42
+ /**
43
+ * Create the minimal theme configuration.
44
+ *
45
+ * @param options - Optional overrides for components, CSS variables, or color themes
46
+ * @returns A JantTheme configuration object
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { createApp } from "@jant/core";
51
+ * import { minimalTheme } from "@jant/core";
52
+ *
53
+ * export default createApp({
54
+ * theme: minimalTheme(), // re-exported as minimalTheme from @jant/core
55
+ * });
56
+ * ```
57
+ */
58
+ export function theme(options?: ThemeOptions): JantTheme {
59
+ return {
60
+ name: "minimal",
61
+ components: {
62
+ SiteLayout,
63
+ HomePage,
64
+ PostPage,
65
+ SinglePage,
66
+ ArchivePage,
67
+ SearchPage,
68
+ CollectionPage,
69
+ NoteCard,
70
+ ArticleCard,
71
+ LinkCard,
72
+ QuoteCard,
73
+ ImageCard,
74
+ ThreadPreview,
75
+ TimelineFeed,
76
+ ...options?.components,
77
+ },
78
+ cssVariables: {
79
+ ...options?.cssVariables,
80
+ },
81
+ colorThemes: options?.colorThemes,
82
+ };
83
+ }
@@ -1,15 +1,14 @@
1
1
  /**
2
- * Default Archive Page Component
2
+ * Minimal Theme - Archive Page
3
3
  *
4
- * Renders posts grouped by year-month with type filter and cursor pagination.
5
- * Theme authors can replace this entirely via ThemeComponents.ArchivePage.
4
+ * Date-first list with type filter and cursor pagination.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
- import type { ArchivePageProps } from "../../types.js";
11
- import { POST_TYPES } from "../../types.js";
12
- import { Pagination as DefaultPagination } from "../components/Pagination.js";
9
+ import type { ArchivePageProps } from "../../../types.js";
10
+ import { POST_TYPES } from "../../../types.js";
11
+ import { Pagination as DefaultPagination } from "../../../theme/components/Pagination.js";
13
12
 
14
13
  function getTypeLabel(type: string): string {
15
14
  const { t } = useLingui();
@@ -83,11 +82,10 @@ export const ArchivePage: FC<ArchivePageProps> = ({
83
82
  <header class="mb-8">
84
83
  <h1 class="text-2xl font-semibold">{title}</h1>
85
84
 
86
- {/* Type filter */}
87
85
  <nav class="flex flex-wrap gap-2 mt-4">
88
86
  <a
89
87
  href="/archive"
90
- class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
88
+ class={`text-sm ${!type ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
91
89
  >
92
90
  {t({
93
91
  message: "All",
@@ -98,7 +96,7 @@ export const ArchivePage: FC<ArchivePageProps> = ({
98
96
  <a
99
97
  key={typeKey}
100
98
  href={`/archive?type=${typeKey}`}
101
- class={`badge ${type === typeKey ? "badge-primary" : "badge-outline"}`}
99
+ class={`text-sm ${type === typeKey ? "font-medium text-foreground" : "text-muted-foreground hover:text-foreground"}`}
102
100
  >
103
101
  {getTypeLabelPlural(typeKey)}
104
102
  </a>
@@ -136,7 +134,7 @@ export const ArchivePage: FC<ArchivePageProps> = ({
136
134
  `Post #${post.id}`}
137
135
  </a>
138
136
  {!type && (
139
- <span class="ml-2 badge-outline text-xs">
137
+ <span class="ml-2 text-xs text-muted-foreground">
140
138
  {getTypeLabel(post.type)}
141
139
  </span>
142
140
  )}
@@ -149,7 +147,6 @@ export const ArchivePage: FC<ArchivePageProps> = ({
149
147
  )}
150
148
  </main>
151
149
 
152
- {/* Pagination */}
153
150
  <PaginationComponent
154
151
  baseUrl={type ? `/archive?type=${type}` : "/archive"}
155
152
  hasMore={hasMore}
@@ -1,13 +1,12 @@
1
1
  /**
2
- * Default Collection Page Component
2
+ * Minimal Theme - Collection Page
3
3
  *
4
- * Renders a collection with its posts.
5
- * Theme authors can replace this entirely via ThemeComponents.CollectionPage.
4
+ * Simple list of posts in a collection.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
- import type { CollectionPageProps } from "../../types.js";
9
+ import type { CollectionPageProps } from "../../../types.js";
11
10
 
12
11
  export const CollectionPage: FC<CollectionPageProps> = ({
13
12
  collection,
@@ -24,7 +23,7 @@ export const CollectionPage: FC<CollectionPageProps> = ({
24
23
  )}
25
24
  </header>
26
25
 
27
- <main class="flex flex-col gap-6">
26
+ <main class="flex flex-col">
28
27
  {posts.length === 0 ? (
29
28
  <p class="text-muted-foreground">
30
29
  {t({
@@ -33,8 +32,9 @@ export const CollectionPage: FC<CollectionPageProps> = ({
33
32
  })}
34
33
  </p>
35
34
  ) : (
36
- posts.map((post) => (
35
+ posts.map((post, i) => (
37
36
  <article key={post.id} class="h-entry">
37
+ {i > 0 && <hr class="my-6 border-border" />}
38
38
  {post.title && (
39
39
  <h2 class="p-name text-lg font-medium mb-2">
40
40
  <a href={post.permalink} class="u-url hover:underline">
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Default Home Page Component
2
+ * Minimal Theme - Home Page
3
3
  *
4
4
  * Renders the timeline feed with thread previews.
5
- * Theme authors can replace this entirely via ThemeComponents.HomePage.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
- import type { HomePageProps } from "../../types.js";
11
- import { TimelineFeed as DefaultTimelineFeed } from "../components/timeline/TimelineFeed.js";
9
+ import type { HomePageProps } from "../../../types.js";
10
+ import { TimelineFeed as DefaultTimelineFeed } from "../timeline/TimelineFeed.js";
12
11
 
13
12
  export const HomePage: FC<HomePageProps> = ({
14
13
  items,
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Default Post Page Component
2
+ * Minimal Theme - Post Page
3
3
  *
4
- * Renders a single post with media gallery.
5
- * Theme authors can replace this entirely via ThemeComponents.PostPage.
4
+ * Clean article layout for a single post.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
- import type { PostPageProps } from "../../types.js";
11
- import { MediaGallery as DefaultMediaGallery } from "../components/MediaGallery.js";
9
+ import type { PostPageProps } from "../../../types.js";
10
+ import { MediaGallery as DefaultMediaGallery } from "../../../theme/components/MediaGallery.js";
12
11
 
13
12
  export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
14
13
  const { t } = useLingui();
@@ -28,11 +27,11 @@ export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
28
27
 
29
28
  {post.media.length > 0 && <Gallery attachments={post.media} />}
30
29
 
31
- <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
30
+ <footer class="mt-8 pt-4 border-t border-border text-sm text-muted-foreground">
32
31
  <time class="dt-published" datetime={post.publishedAt}>
33
32
  {post.publishedAtFormatted}
34
33
  </time>
35
- <a href={post.permalink} class="u-url ml-4">
34
+ <a href={post.permalink} class="u-url ml-4 hover:underline">
36
35
  {t({
37
36
  message: "Permalink",
38
37
  comment: "@context: Link to permanent URL of post",
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Default Search Page Component
2
+ * Minimal Theme - Search Page
3
3
  *
4
- * Renders search form and results with page-based pagination.
5
- * Theme authors can replace this entirely via ThemeComponents.SearchPage.
4
+ * Minimal search form + results with page-based pagination.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
8
  import { useLingui } from "@lingui/react/macro";
10
- import type { SearchPageProps } from "../../types.js";
11
- import { PagePagination as DefaultPagePagination } from "../components/Pagination.js";
9
+ import type { SearchPageProps } from "../../../types.js";
10
+ import { PagePagination as DefaultPagePagination } from "../../../theme/components/Pagination.js";
12
11
 
13
12
  export const SearchPage: FC<SearchPageProps> = ({
14
13
  query,
@@ -30,7 +29,6 @@ export const SearchPage: FC<SearchPageProps> = ({
30
29
  <div>
31
30
  <h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
32
31
 
33
- {/* Search form */}
34
32
  <form method="get" action="/search" class="mb-8">
35
33
  <div class="flex gap-2">
36
34
  <input
@@ -53,14 +51,12 @@ export const SearchPage: FC<SearchPageProps> = ({
53
51
  </div>
54
52
  </form>
55
53
 
56
- {/* Error */}
57
54
  {error && (
58
55
  <div class="alert-destructive mb-6">
59
56
  <h2>{error}</h2>
60
57
  </div>
61
58
  )}
62
59
 
63
- {/* Results */}
64
60
  {query && !error && (
65
61
  <div>
66
62
  <p class="text-sm text-muted-foreground mb-4">
@@ -85,12 +81,9 @@ export const SearchPage: FC<SearchPageProps> = ({
85
81
  <>
86
82
  <div class="flex flex-col gap-4">
87
83
  {results.map((result) => (
88
- <article
89
- key={result.post.id}
90
- class="p-4 rounded-lg border hover:border-primary"
91
- >
92
- <a href={result.post.permalink} class="block">
93
- <h2 class="font-medium hover:underline">
84
+ <article key={result.post.id} class="py-3">
85
+ <a href={result.post.permalink} class="block group">
86
+ <h2 class="font-medium group-hover:underline">
94
87
  {result.post.title ||
95
88
  result.post.content?.slice(0, 60) ||
96
89
  `Post #${result.post.id}`}
@@ -98,13 +91,14 @@ export const SearchPage: FC<SearchPageProps> = ({
98
91
 
99
92
  {result.snippet && (
100
93
  <p
101
- class="text-sm text-muted-foreground mt-2 line-clamp-2"
94
+ class="text-sm text-muted-foreground mt-1 line-clamp-2"
102
95
  dangerouslySetInnerHTML={{ __html: result.snippet }}
103
96
  />
104
97
  )}
105
98
 
106
- <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
107
- <span class="badge-outline">{result.post.type}</span>
99
+ <footer class="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
100
+ <span>{result.post.type}</span>
101
+ <span>&middot;</span>
108
102
  <time datetime={result.post.publishedAt}>
109
103
  {result.post.publishedAtFormatted}
110
104
  </time>
@@ -1,18 +1,17 @@
1
1
  /**
2
- * Default Single Page Component
2
+ * Minimal Theme - Single Page
3
3
  *
4
- * Renders a custom page (type "page").
5
- * Theme authors can replace this entirely via ThemeComponents.SinglePage.
4
+ * Simple page content layout for type="page" posts.
6
5
  */
7
6
 
8
7
  import type { FC } from "hono/jsx";
9
- import type { SinglePageProps } from "../../types.js";
8
+ import type { SinglePageProps } from "../../../types.js";
10
9
 
11
10
  export const SinglePage: FC<SinglePageProps> = ({ page }) => {
12
11
  return (
13
12
  <article class="h-entry">
14
13
  {page.title && (
15
- <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
14
+ <h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
16
15
  )}
17
16
 
18
17
  <div
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Minimal Theme - Article Card
3
+ *
4
+ * Title + excerpt, borderless, for type="article" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+
10
+ export const ArticleCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
+ return (
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
13
+ {post.title && (
14
+ <h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-lg"}`}>
15
+ <a href={post.permalink} class="u-url hover:underline">
16
+ {post.title}
17
+ </a>
18
+ </h2>
19
+ )}
20
+ {!compact && post.excerpt && (
21
+ <p class="e-content text-muted-foreground mt-1 line-clamp-3">
22
+ {post.excerpt}
23
+ </p>
24
+ )}
25
+ <footer class="mt-2">
26
+ <a
27
+ href={post.permalink}
28
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
29
+ >
30
+ <time class="dt-published" datetime={post.publishedAt}>
31
+ {post.publishedAtFormatted}
32
+ </time>
33
+ </a>
34
+ </footer>
35
+ </article>
36
+ );
37
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Minimal Theme - Image Card
3
+ *
4
+ * Inline images with no card frame for type="image" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+ import { MediaGallery } from "../../../theme/components/MediaGallery.js";
10
+
11
+ export const ImageCard: FC<TimelineCardProps> = ({ post, compact }) => {
12
+ if (compact) {
13
+ return (
14
+ <article class="h-entry text-sm">
15
+ {post.title && (
16
+ <h2 class="p-name font-medium text-sm">
17
+ <a href={post.permalink} class="u-url hover:underline">
18
+ {post.title}
19
+ </a>
20
+ </h2>
21
+ )}
22
+ {post.contentHtml && (
23
+ <div
24
+ class="e-content prose prose-sm text-muted-foreground"
25
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
26
+ />
27
+ )}
28
+ <footer class="mt-1">
29
+ <a
30
+ href={post.permalink}
31
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
32
+ >
33
+ <time class="dt-published" datetime={post.publishedAt}>
34
+ {post.publishedAtFormatted}
35
+ </time>
36
+ </a>
37
+ </footer>
38
+ </article>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <article class="h-entry">
44
+ {post.contentHtml && (
45
+ <div
46
+ class="e-content prose prose-sm"
47
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
48
+ />
49
+ )}
50
+ {post.media.length > 0 && <MediaGallery attachments={post.media} />}
51
+ <footer class="mt-2">
52
+ <a
53
+ href={post.permalink}
54
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
55
+ >
56
+ <time class="dt-published" datetime={post.publishedAt}>
57
+ {post.publishedAtFormatted}
58
+ </time>
59
+ </a>
60
+ </footer>
61
+ </article>
62
+ );
63
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Minimal Theme - Link Card
3
+ *
4
+ * Subtle external link indicator for type="link" posts.
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import type { TimelineCardProps } from "../../../types.js";
9
+
10
+ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
+ return (
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
13
+ {post.title && (
14
+ <h2 class={`p-name font-semibold ${compact ? "text-sm" : "text-base"}`}>
15
+ <a
16
+ href={post.sourceUrl || post.permalink}
17
+ class="u-url hover:underline"
18
+ target={post.sourceUrl ? "_blank" : undefined}
19
+ rel={post.sourceUrl ? "noopener noreferrer" : undefined}
20
+ >
21
+ {post.title}
22
+ </a>
23
+ </h2>
24
+ )}
25
+ {post.sourceDomain && (
26
+ <div class="text-xs text-muted-foreground mt-0.5">
27
+ &#8599; {post.sourceDomain}
28
+ </div>
29
+ )}
30
+ {!compact && post.contentHtml && (
31
+ <div
32
+ class="e-content prose prose-sm text-muted-foreground mt-1"
33
+ dangerouslySetInnerHTML={{ __html: post.contentHtml }}
34
+ />
35
+ )}
36
+ <footer class="mt-2">
37
+ <a
38
+ href={post.permalink}
39
+ class="text-xs text-muted-foreground hover:text-foreground"
40
+ >
41
+ <time class="dt-published" datetime={post.publishedAt}>
42
+ {post.publishedAtFormatted}
43
+ </time>
44
+ </a>
45
+ </footer>
46
+ </article>
47
+ );
48
+ };
@@ -1,29 +1,30 @@
1
1
  /**
2
- * Note Card Component
2
+ * Minimal Theme - Note Card
3
3
  *
4
- * Text-first, minimal card for type="note" posts.
4
+ * Borderless, content-first card for type="note" posts.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
8
  import type { TimelineCardProps } from "../../../types.js";
9
- import { MediaGallery } from "../MediaGallery.js";
9
+ import { MediaGallery } from "../../../theme/components/MediaGallery.js";
10
10
 
11
11
  export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
12
12
  return (
13
- <article
14
- class={`h-entry timeline-card${compact ? " timeline-card-compact" : ""}`}
15
- >
13
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
16
14
  {post.contentHtml && (
17
15
  <div
18
- class={`e-content prose ${compact ? "prose-sm" : "prose-sm"}`}
16
+ class={`e-content prose ${compact ? "prose-sm" : ""}`}
19
17
  dangerouslySetInnerHTML={{ __html: post.contentHtml }}
20
18
  />
21
19
  )}
22
20
  {!compact && post.media.length > 0 && (
23
21
  <MediaGallery attachments={post.media} />
24
22
  )}
25
- <footer class="mt-2 text-xs text-muted-foreground">
26
- <a href={post.permalink} class="u-url hover:underline">
23
+ <footer class="mt-2">
24
+ <a
25
+ href={post.permalink}
26
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
27
+ >
27
28
  <time class="dt-published" datetime={post.publishedAt}>
28
29
  {post.publishedAtFormatted}
29
30
  </time>
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Quote Card Component
2
+ * Minimal Theme - Quote Card
3
3
  *
4
- * Blockquote + attribution for type="quote" posts.
4
+ * Subtle blockquote with left border for type="quote" posts.
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
@@ -9,12 +9,10 @@ import type { TimelineCardProps } from "../../../types.js";
9
9
 
10
10
  export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
11
11
  return (
12
- <article
13
- class={`h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`}
14
- >
12
+ <article class={`h-entry${compact ? " text-sm" : ""}`}>
15
13
  {post.contentHtml && (
16
14
  <blockquote
17
- class={`e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`}
15
+ class={`e-content border-l-2 border-muted-foreground/30 pl-4 italic ${compact ? "text-sm" : ""} leading-relaxed`}
18
16
  >
19
17
  <div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
20
18
  </blockquote>
@@ -36,8 +34,11 @@ export const QuoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
36
34
  )}
37
35
  </div>
38
36
  )}
39
- <footer class="mt-2 text-xs text-muted-foreground">
40
- <a href={post.permalink} class="u-url hover:underline">
37
+ <footer class="mt-2">
38
+ <a
39
+ href={post.permalink}
40
+ class="u-url text-xs text-muted-foreground hover:text-foreground"
41
+ >
41
42
  <time class="dt-published" datetime={post.publishedAt}>
42
43
  {post.publishedAtFormatted}
43
44
  </time>