@jant/core 0.3.23 → 0.3.25

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 (248) hide show
  1. package/dist/app.js +50 -26
  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 +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Compose Dialog
3
+ *
4
+ * Full-screen compose dialog for quick post creation.
5
+ * Rendered server-side as part of SiteLayout for authenticated users.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import type { Collection } from "../../types.js";
10
+ import { useLingui } from "@lingui/react/macro";
11
+
12
+ export interface ComposeDialogProps {
13
+ collections?: Collection[];
14
+ }
15
+
16
+ export const ComposeDialog: FC<ComposeDialogProps> = ({ collections }) => {
17
+ const { t } = useLingui();
18
+
19
+ const signals = JSON.stringify({
20
+ format: "note",
21
+ title: "",
22
+ body: "",
23
+ url: "",
24
+ quoteText: "",
25
+ status: "published",
26
+ featured: false,
27
+ pinned: false,
28
+ rating: 0,
29
+ collectionId: 0,
30
+ mediaIds: [],
31
+ _composeLoading: false,
32
+ _showRating: false,
33
+ _showCollection: false,
34
+ }).replace(/</g, "\\u003c");
35
+
36
+ return (
37
+ <dialog
38
+ id="compose-dialog"
39
+ class="compose-dialog backdrop:bg-black/50"
40
+ onclick="event.target === this && this.close()"
41
+ >
42
+ <div class="compose-dialog-inner">
43
+ {/* Header */}
44
+ <header class="compose-dialog-header">
45
+ <button
46
+ type="button"
47
+ class="compose-dialog-close"
48
+ onclick="this.closest('dialog').close()"
49
+ >
50
+ <svg
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ width="20"
53
+ height="20"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ >
61
+ <path d="M18 6 6 18" />
62
+ <path d="M6 6l12 12" />
63
+ </svg>
64
+ </button>
65
+ <h2 class="compose-dialog-title">
66
+ {t({
67
+ message: "New Post",
68
+ comment: "@context: Compose dialog title",
69
+ })}
70
+ </h2>
71
+ <div class="w-5" />
72
+ </header>
73
+
74
+ {/* Form */}
75
+ <section class="compose-dialog-body">
76
+ <form
77
+ data-signals={signals}
78
+ data-on:submit__prevent="@post('/compose')"
79
+ data-indicator="_composeLoading"
80
+ class="flex flex-col gap-3"
81
+ >
82
+ {/* Format tabs */}
83
+ <div class="compose-format-tabs">
84
+ <button
85
+ type="button"
86
+ class="compose-format-tab"
87
+ data-class-compose-format-tab-active="$format === 'note'"
88
+ data-on:click="$format = 'note'"
89
+ >
90
+ {t({
91
+ message: "Note",
92
+ comment: "@context: Compose format tab",
93
+ })}
94
+ </button>
95
+ <button
96
+ type="button"
97
+ class="compose-format-tab"
98
+ data-class-compose-format-tab-active="$format === 'link'"
99
+ data-on:click="$format = 'link'"
100
+ >
101
+ {t({
102
+ message: "Link",
103
+ comment: "@context: Compose format tab",
104
+ })}
105
+ </button>
106
+ <button
107
+ type="button"
108
+ class="compose-format-tab"
109
+ data-class-compose-format-tab-active="$format === 'quote'"
110
+ data-on:click="$format = 'quote'"
111
+ >
112
+ {t({
113
+ message: "Quote",
114
+ comment: "@context: Compose format tab",
115
+ })}
116
+ </button>
117
+ </div>
118
+
119
+ {/* Title input */}
120
+ <input
121
+ type="text"
122
+ data-bind="title"
123
+ class="compose-title-input"
124
+ placeholder={t({
125
+ message: "Title (optional)",
126
+ comment: "@context: Compose title placeholder",
127
+ })}
128
+ />
129
+
130
+ {/* Body textarea */}
131
+ <textarea
132
+ data-bind="body"
133
+ class="compose-body-input"
134
+ placeholder={t({
135
+ message: "What's on your mind?",
136
+ comment: "@context: Compose body placeholder",
137
+ })}
138
+ rows={4}
139
+ />
140
+
141
+ {/* URL input (link/quote) */}
142
+ <div data-show="$format === 'link' || $format === 'quote'">
143
+ <input
144
+ type="url"
145
+ data-bind="url"
146
+ class="input text-sm"
147
+ placeholder="https://..."
148
+ />
149
+ </div>
150
+
151
+ {/* Quote text (quote format) */}
152
+ <div data-show="$format === 'quote'">
153
+ <textarea
154
+ data-bind="quoteText"
155
+ class="textarea text-sm"
156
+ placeholder={t({
157
+ message: "The text being quoted...",
158
+ comment: "@context: Compose quote text placeholder",
159
+ })}
160
+ rows={2}
161
+ />
162
+ </div>
163
+
164
+ {/* Rating picker (toggleable) */}
165
+ <div data-show="$_showRating" class="field">
166
+ <label class="label text-sm">
167
+ {t({
168
+ message: "Rating",
169
+ comment: "@context: Compose rating field",
170
+ })}
171
+ </label>
172
+ <select data-bind="rating" class="select text-sm">
173
+ <option value="0">
174
+ {t({
175
+ message: "None",
176
+ comment: "@context: No rating selected",
177
+ })}
178
+ </option>
179
+ <option value="1">1</option>
180
+ <option value="2">2</option>
181
+ <option value="3">3</option>
182
+ <option value="4">4</option>
183
+ <option value="5">5</option>
184
+ </select>
185
+ </div>
186
+
187
+ {/* Collection picker (toggleable) */}
188
+ {collections && collections.length > 0 && (
189
+ <div data-show="$_showCollection" class="field">
190
+ <label class="label text-sm">
191
+ {t({
192
+ message: "Collection",
193
+ comment: "@context: Compose collection field",
194
+ })}
195
+ </label>
196
+ <select data-bind="collectionId" class="select text-sm">
197
+ <option value="0">
198
+ {t({
199
+ message: "None",
200
+ comment: "@context: No collection selected",
201
+ })}
202
+ </option>
203
+ {collections.map((col) => (
204
+ <option key={col.id} value={col.id}>
205
+ {col.title}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ </div>
210
+ )}
211
+
212
+ {/* Toolbar */}
213
+ <div class="compose-toolbar">
214
+ <div class="flex gap-1">
215
+ {/* Media button */}
216
+ <button
217
+ type="button"
218
+ class="compose-toolbar-btn"
219
+ title={t({
220
+ message: "Add Media",
221
+ comment: "@context: Compose toolbar - add media",
222
+ })}
223
+ data-on:click="document.getElementById('compose-media-picker').showModal(); fetch('/dash/media/picker').then(r => r.text()).then(html => document.getElementById('compose-media-grid').innerHTML = html)"
224
+ >
225
+ <svg
226
+ xmlns="http://www.w3.org/2000/svg"
227
+ width="18"
228
+ height="18"
229
+ viewBox="0 0 24 24"
230
+ fill="none"
231
+ stroke="currentColor"
232
+ stroke-width="2"
233
+ stroke-linecap="round"
234
+ stroke-linejoin="round"
235
+ >
236
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
237
+ <circle cx="9" cy="9" r="2" />
238
+ <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
239
+ </svg>
240
+ </button>
241
+
242
+ {/* Rating toggle */}
243
+ <button
244
+ type="button"
245
+ class="compose-toolbar-btn"
246
+ title={t({
247
+ message: "Rating",
248
+ comment: "@context: Compose toolbar - toggle rating",
249
+ })}
250
+ data-on:click="$_showRating = !$_showRating"
251
+ >
252
+ <svg
253
+ xmlns="http://www.w3.org/2000/svg"
254
+ width="18"
255
+ height="18"
256
+ viewBox="0 0 24 24"
257
+ fill="none"
258
+ stroke="currentColor"
259
+ stroke-width="2"
260
+ stroke-linecap="round"
261
+ stroke-linejoin="round"
262
+ >
263
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
264
+ </svg>
265
+ </button>
266
+
267
+ {/* Collection toggle */}
268
+ {collections && collections.length > 0 && (
269
+ <button
270
+ type="button"
271
+ class="compose-toolbar-btn"
272
+ title={t({
273
+ message: "Collection",
274
+ comment: "@context: Compose toolbar - toggle collection",
275
+ })}
276
+ data-on:click="$_showCollection = !$_showCollection"
277
+ >
278
+ <svg
279
+ xmlns="http://www.w3.org/2000/svg"
280
+ width="18"
281
+ height="18"
282
+ viewBox="0 0 24 24"
283
+ fill="none"
284
+ stroke="currentColor"
285
+ stroke-width="2"
286
+ stroke-linecap="round"
287
+ stroke-linejoin="round"
288
+ >
289
+ <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
290
+ </svg>
291
+ </button>
292
+ )}
293
+ </div>
294
+ </div>
295
+
296
+ {/* Footer: checkboxes + submit */}
297
+ <div class="compose-dialog-footer">
298
+ <div class="flex gap-3">
299
+ <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
300
+ <input
301
+ type="checkbox"
302
+ class="checkbox"
303
+ data-bind="featured"
304
+ />
305
+ {t({
306
+ message: "Featured",
307
+ comment: "@context: Compose checkbox - mark as featured",
308
+ })}
309
+ </label>
310
+ <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
311
+ <input type="checkbox" class="checkbox" data-bind="pinned" />
312
+ {t({
313
+ message: "Pinned",
314
+ comment: "@context: Compose checkbox - pin to top",
315
+ })}
316
+ </label>
317
+ </div>
318
+ <div class="flex gap-2">
319
+ <button
320
+ type="button"
321
+ class="btn-outline text-sm"
322
+ data-attr-disabled="$_composeLoading"
323
+ data-on:click="$status = 'draft'; document.querySelector('#compose-dialog form').requestSubmit()"
324
+ >
325
+ <span data-show="!$_composeLoading">
326
+ {t({
327
+ message: "Draft",
328
+ comment: "@context: Compose button - save as draft",
329
+ })}
330
+ </span>
331
+ <span data-show="$_composeLoading">...</span>
332
+ </button>
333
+ <button
334
+ type="submit"
335
+ class="btn text-sm"
336
+ data-attr-disabled="$_composeLoading"
337
+ >
338
+ <span data-show="!$_composeLoading">
339
+ {t({
340
+ message: "Post",
341
+ comment: "@context: Compose button - publish post",
342
+ })}
343
+ </span>
344
+ <span data-show="$_composeLoading">
345
+ {t({
346
+ message: "Posting...",
347
+ comment: "@context: Compose loading text while posting",
348
+ })}
349
+ </span>
350
+ </button>
351
+ </div>
352
+ </div>
353
+ </form>
354
+ </section>
355
+ </div>
356
+
357
+ {/* Nested media picker dialog */}
358
+ <dialog
359
+ id="compose-media-picker"
360
+ class="p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50"
361
+ onclick="event.target === this && this.close()"
362
+ >
363
+ <div class="flex items-center justify-between mb-4">
364
+ <h2 class="text-lg font-semibold">
365
+ {t({
366
+ message: "Select Media",
367
+ comment: "@context: Media picker dialog title",
368
+ })}
369
+ </h2>
370
+ <button
371
+ type="button"
372
+ class="btn-outline text-sm"
373
+ onclick="this.closest('dialog').close()"
374
+ >
375
+ {t({
376
+ message: "Done",
377
+ comment: "@context: Close media picker button",
378
+ })}
379
+ </button>
380
+ </div>
381
+ <div
382
+ id="compose-media-grid"
383
+ class="grid grid-cols-4 gap-2 max-h-96 overflow-y-auto"
384
+ >
385
+ <p class="text-muted-foreground text-sm col-span-4">
386
+ {t({
387
+ message: "Loading...",
388
+ comment: "@context: Loading state for media picker",
389
+ })}
390
+ </p>
391
+ </div>
392
+ </dialog>
393
+ </dialog>
394
+ );
395
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Compose Prompt
3
+ *
4
+ * "What's new?" prompt bar at the top of the content area.
5
+ * Clicking it opens the compose dialog.
6
+ */
7
+
8
+ import type { FC } from "hono/jsx";
9
+ import { useLingui } from "@lingui/react/macro";
10
+
11
+ export const ComposePrompt: FC = () => {
12
+ const { t } = useLingui();
13
+
14
+ return (
15
+ <div class="compose-prompt">
16
+ <button
17
+ type="button"
18
+ class="compose-prompt-trigger"
19
+ onclick="document.getElementById('compose-dialog').showModal()"
20
+ >
21
+ <span class="compose-prompt-avatar">
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="16"
25
+ height="16"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="2"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ >
33
+ <path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
34
+ </svg>
35
+ </span>
36
+ <span class="compose-prompt-text">
37
+ {t({
38
+ message: "What's new?",
39
+ comment: "@context: Compose prompt placeholder text",
40
+ })}
41
+ </span>
42
+ </button>
43
+ <button
44
+ type="button"
45
+ class="compose-prompt-post-btn"
46
+ onclick="document.getElementById('compose-dialog').showModal()"
47
+ >
48
+ {t({
49
+ message: "Post",
50
+ comment: "@context: Compose prompt post button",
51
+ })}
52
+ </button>
53
+ </div>
54
+ );
55
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Format Badge Component
3
+ *
4
+ * Displays a badge indicating the format of a post (note, link, quote).
5
+ */
6
+
7
+ import type { FC } from "hono/jsx";
8
+ import { useLingui } from "@lingui/react/macro";
9
+ import type { Format } from "../../types.js";
10
+
11
+ export interface FormatBadgeProps {
12
+ type: Format;
13
+ }
14
+
15
+ export const FormatBadge: FC<FormatBadgeProps> = ({ type }) => {
16
+ const { t } = useLingui();
17
+
18
+ const labels: Record<Format, string> = {
19
+ note: t({ message: "Note", comment: "@context: Post format badge - note" }),
20
+ link: t({ message: "Link", comment: "@context: Post format badge - link" }),
21
+ quote: t({
22
+ message: "Quote",
23
+ comment: "@context: Post format badge - quote",
24
+ }),
25
+ };
26
+
27
+ return <span class="badge-outline">{labels[type]}</span>;
28
+ };
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Page Creation/Edit Form
3
3
  *
4
- * For managing custom pages (posts with type="page")
4
+ * For managing standalone pages (about, now, etc.)
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
- import type { Post } from "../../types.js";
8
+ import type { Page } from "../../types.js";
9
9
  import { useLingui } from "@lingui/react/macro";
10
10
 
11
11
  export interface PageFormProps {
12
- page?: Post;
12
+ page?: Page;
13
13
  action: string;
14
14
  cancelUrl?: string;
15
15
  }
@@ -24,9 +24,9 @@ export const PageForm: FC<PageFormProps> = ({
24
24
 
25
25
  const signals = JSON.stringify({
26
26
  title: page?.title ?? "",
27
- path: page?.path ?? "",
28
- content: page?.content ?? "",
29
- visibility: page?.visibility ?? "unlisted",
27
+ slug: page?.slug ?? "",
28
+ body: page?.body ?? "",
29
+ status: page?.status ?? "published",
30
30
  }).replace(/</g, "\\u003c");
31
31
 
32
32
  return (
@@ -58,25 +58,25 @@ export const PageForm: FC<PageFormProps> = ({
58
58
  />
59
59
  </div>
60
60
 
61
- {/* Path */}
61
+ {/* Slug */}
62
62
  <div class="field">
63
63
  <label class="label">
64
64
  {t({
65
- message: "Path",
66
- comment: "@context: Page form field label - URL path",
65
+ message: "Slug",
66
+ comment: "@context: Page form field label - URL slug",
67
67
  })}
68
68
  </label>
69
69
  <div class="flex items-center gap-2">
70
70
  <span class="text-muted-foreground">/</span>
71
71
  <input
72
72
  type="text"
73
- data-bind="path"
73
+ data-bind="slug"
74
74
  class="input flex-1"
75
75
  placeholder="about"
76
76
  pattern="[a-z0-9\-]+"
77
77
  title={t({
78
78
  message: "Lowercase letters, numbers, and hyphens only",
79
- comment: "@context: Page path validation message",
79
+ comment: "@context: Page slug validation message",
80
80
  })}
81
81
  required
82
82
  />
@@ -85,12 +85,12 @@ export const PageForm: FC<PageFormProps> = ({
85
85
  {t({
86
86
  message:
87
87
  "The URL path for this page. Use lowercase letters, numbers, and hyphens.",
88
- comment: "@context: Page path helper text",
88
+ comment: "@context: Page slug helper text",
89
89
  })}
90
90
  </p>
91
91
  </div>
92
92
 
93
- {/* Content */}
93
+ {/* Body */}
94
94
  <div class="field">
95
95
  <label class="label">
96
96
  {t({
@@ -99,7 +99,7 @@ export const PageForm: FC<PageFormProps> = ({
99
99
  })}
100
100
  </label>
101
101
  <textarea
102
- data-bind="content"
102
+ data-bind="body"
103
103
  class="textarea min-h-48"
104
104
  placeholder={t({
105
105
  message: "Page content (Markdown supported)...",
@@ -107,11 +107,11 @@ export const PageForm: FC<PageFormProps> = ({
107
107
  })}
108
108
  required
109
109
  >
110
- {page?.content ?? ""}
110
+ {page?.body ?? ""}
111
111
  </textarea>
112
112
  </div>
113
113
 
114
- {/* Visibility */}
114
+ {/* Status */}
115
115
  <div class="field">
116
116
  <label class="label">
117
117
  {t({
@@ -119,17 +119,17 @@ export const PageForm: FC<PageFormProps> = ({
119
119
  comment: "@context: Page form field label - publish status",
120
120
  })}
121
121
  </label>
122
- <select data-bind="visibility" class="select">
122
+ <select data-bind="status" class="select">
123
123
  <option
124
- value="unlisted"
125
- selected={page?.visibility === "unlisted" || !page}
124
+ value="published"
125
+ selected={page?.status === "published" || !page}
126
126
  >
127
127
  {t({
128
128
  message: "Published",
129
129
  comment: "@context: Page status option - published",
130
130
  })}
131
131
  </option>
132
- <option value="draft" selected={page?.visibility === "draft"}>
132
+ <option value="draft" selected={page?.status === "draft"}>
133
133
  {t({
134
134
  message: "Draft",
135
135
  comment: "@context: Page status option - draft",
@@ -139,7 +139,7 @@ export const PageForm: FC<PageFormProps> = ({
139
139
  <p class="text-xs text-muted-foreground mt-1">
140
140
  {t({
141
141
  message:
142
- "Published pages are accessible via their path. Drafts are not visible.",
142
+ "Published pages are accessible via their slug. Drafts are not visible.",
143
143
  comment: "@context: Page status helper text",
144
144
  })}
145
145
  </p>