@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Timezone List
3
+ *
4
+ * Provides a curated list of timezones for the settings UI
5
+ * and a helper to map IANA timezone names to our list entries.
6
+ */
7
+
8
+ export interface TimezoneEntry {
9
+ /** Display value stored in settings */
10
+ value: string;
11
+ /** Human-readable label for the select UI */
12
+ label: string;
13
+ /** UTC offset string, e.g. "+08:00" */
14
+ offset: string;
15
+ /** IANA timezone names that map to this entry */
16
+ iana: string[];
17
+ }
18
+
19
+ export const TIMEZONES: TimezoneEntry[] = [
20
+ {
21
+ value: "International Date Line West",
22
+ label: "(UTC-12:00) International Date Line West",
23
+ offset: "-12:00",
24
+ iana: ["Etc/GMT+12"],
25
+ },
26
+ {
27
+ value: "American Samoa",
28
+ label: "(UTC-11:00) American Samoa",
29
+ offset: "-11:00",
30
+ iana: ["Pacific/Pago_Pago", "Pacific/Midway", "Etc/GMT+11"],
31
+ },
32
+ {
33
+ value: "Hawaii",
34
+ label: "(UTC-10:00) Hawaii",
35
+ offset: "-10:00",
36
+ iana: ["Pacific/Honolulu", "Etc/GMT+10"],
37
+ },
38
+ {
39
+ value: "Alaska",
40
+ label: "(UTC-09:00) Alaska",
41
+ offset: "-09:00",
42
+ iana: ["America/Anchorage", "America/Juneau", "America/Nome"],
43
+ },
44
+ {
45
+ value: "Pacific Time (US & Canada)",
46
+ label: "(UTC-08:00) Pacific Time (US & Canada)",
47
+ offset: "-08:00",
48
+ iana: ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
49
+ },
50
+ {
51
+ value: "Mountain Time (US & Canada)",
52
+ label: "(UTC-07:00) Mountain Time (US & Canada)",
53
+ offset: "-07:00",
54
+ iana: [
55
+ "America/Denver",
56
+ "America/Edmonton",
57
+ "America/Phoenix",
58
+ "America/Boise",
59
+ ],
60
+ },
61
+ {
62
+ value: "Central Time (US & Canada)",
63
+ label: "(UTC-06:00) Central Time (US & Canada)",
64
+ offset: "-06:00",
65
+ iana: [
66
+ "America/Chicago",
67
+ "America/Winnipeg",
68
+ "America/Mexico_City",
69
+ "America/Guatemala",
70
+ ],
71
+ },
72
+ {
73
+ value: "Eastern Time (US & Canada)",
74
+ label: "(UTC-05:00) Eastern Time (US & Canada)",
75
+ offset: "-05:00",
76
+ iana: [
77
+ "America/New_York",
78
+ "America/Toronto",
79
+ "America/Detroit",
80
+ "America/Indiana/Indianapolis",
81
+ "America/Bogota",
82
+ "America/Lima",
83
+ ],
84
+ },
85
+ {
86
+ value: "Atlantic Time (Canada)",
87
+ label: "(UTC-04:00) Atlantic Time (Canada)",
88
+ offset: "-04:00",
89
+ iana: [
90
+ "America/Halifax",
91
+ "America/Caracas",
92
+ "America/Santiago",
93
+ "America/La_Paz",
94
+ ],
95
+ },
96
+ {
97
+ value: "Newfoundland",
98
+ label: "(UTC-03:30) Newfoundland",
99
+ offset: "-03:30",
100
+ iana: ["America/St_Johns"],
101
+ },
102
+ {
103
+ value: "Buenos Aires",
104
+ label: "(UTC-03:00) Buenos Aires",
105
+ offset: "-03:00",
106
+ iana: [
107
+ "America/Argentina/Buenos_Aires",
108
+ "America/Sao_Paulo",
109
+ "America/Montevideo",
110
+ ],
111
+ },
112
+ {
113
+ value: "Mid-Atlantic",
114
+ label: "(UTC-02:00) Mid-Atlantic",
115
+ offset: "-02:00",
116
+ iana: ["Atlantic/South_Georgia", "Etc/GMT+2"],
117
+ },
118
+ {
119
+ value: "Azores",
120
+ label: "(UTC-01:00) Azores",
121
+ offset: "-01:00",
122
+ iana: ["Atlantic/Azores", "Atlantic/Cape_Verde"],
123
+ },
124
+ {
125
+ value: "UTC",
126
+ label: "(UTC+00:00) UTC",
127
+ offset: "+00:00",
128
+ iana: ["Etc/UTC", "UTC", "Etc/GMT", "Africa/Accra"],
129
+ },
130
+ {
131
+ value: "London",
132
+ label: "(UTC+00:00) London",
133
+ offset: "+00:00",
134
+ iana: ["Europe/London", "Europe/Dublin", "Europe/Lisbon"],
135
+ },
136
+ {
137
+ value: "Central European Time",
138
+ label: "(UTC+01:00) Central European Time",
139
+ offset: "+01:00",
140
+ iana: [
141
+ "Europe/Paris",
142
+ "Europe/Berlin",
143
+ "Europe/Amsterdam",
144
+ "Europe/Rome",
145
+ "Europe/Madrid",
146
+ "Europe/Brussels",
147
+ "Europe/Vienna",
148
+ "Europe/Warsaw",
149
+ "Europe/Prague",
150
+ "Europe/Stockholm",
151
+ "Europe/Oslo",
152
+ "Europe/Copenhagen",
153
+ "Europe/Zurich",
154
+ "Africa/Lagos",
155
+ ],
156
+ },
157
+ {
158
+ value: "Eastern European Time",
159
+ label: "(UTC+02:00) Eastern European Time",
160
+ offset: "+02:00",
161
+ iana: [
162
+ "Europe/Helsinki",
163
+ "Europe/Athens",
164
+ "Europe/Bucharest",
165
+ "Europe/Istanbul",
166
+ "Africa/Cairo",
167
+ "Africa/Johannesburg",
168
+ "Asia/Jerusalem",
169
+ "Asia/Beirut",
170
+ ],
171
+ },
172
+ {
173
+ value: "Moscow",
174
+ label: "(UTC+03:00) Moscow",
175
+ offset: "+03:00",
176
+ iana: [
177
+ "Europe/Moscow",
178
+ "Europe/Minsk",
179
+ "Asia/Baghdad",
180
+ "Asia/Riyadh",
181
+ "Africa/Nairobi",
182
+ "Asia/Kuwait",
183
+ ],
184
+ },
185
+ {
186
+ value: "Tehran",
187
+ label: "(UTC+03:30) Tehran",
188
+ offset: "+03:30",
189
+ iana: ["Asia/Tehran"],
190
+ },
191
+ {
192
+ value: "Dubai",
193
+ label: "(UTC+04:00) Dubai",
194
+ offset: "+04:00",
195
+ iana: ["Asia/Dubai", "Asia/Muscat", "Asia/Baku", "Asia/Tbilisi"],
196
+ },
197
+ {
198
+ value: "Kabul",
199
+ label: "(UTC+04:30) Kabul",
200
+ offset: "+04:30",
201
+ iana: ["Asia/Kabul"],
202
+ },
203
+ {
204
+ value: "Karachi",
205
+ label: "(UTC+05:00) Karachi",
206
+ offset: "+05:00",
207
+ iana: ["Asia/Karachi", "Asia/Tashkent", "Asia/Yekaterinburg"],
208
+ },
209
+ {
210
+ value: "Mumbai",
211
+ label: "(UTC+05:30) Mumbai",
212
+ offset: "+05:30",
213
+ iana: ["Asia/Kolkata", "Asia/Calcutta", "Asia/Colombo"],
214
+ },
215
+ {
216
+ value: "Kathmandu",
217
+ label: "(UTC+05:45) Kathmandu",
218
+ offset: "+05:45",
219
+ iana: ["Asia/Kathmandu"],
220
+ },
221
+ {
222
+ value: "Dhaka",
223
+ label: "(UTC+06:00) Dhaka",
224
+ offset: "+06:00",
225
+ iana: ["Asia/Dhaka", "Asia/Almaty", "Asia/Omsk"],
226
+ },
227
+ {
228
+ value: "Yangon",
229
+ label: "(UTC+06:30) Yangon",
230
+ offset: "+06:30",
231
+ iana: ["Asia/Yangon", "Asia/Rangoon"],
232
+ },
233
+ {
234
+ value: "Bangkok",
235
+ label: "(UTC+07:00) Bangkok",
236
+ offset: "+07:00",
237
+ iana: [
238
+ "Asia/Bangkok",
239
+ "Asia/Jakarta",
240
+ "Asia/Ho_Chi_Minh",
241
+ "Asia/Krasnoyarsk",
242
+ ],
243
+ },
244
+ {
245
+ value: "Beijing",
246
+ label: "(UTC+08:00) Beijing",
247
+ offset: "+08:00",
248
+ iana: [
249
+ "Asia/Shanghai",
250
+ "Asia/Hong_Kong",
251
+ "Asia/Taipei",
252
+ "Asia/Singapore",
253
+ "Asia/Kuala_Lumpur",
254
+ "Asia/Makassar",
255
+ "Asia/Irkutsk",
256
+ "Australia/Perth",
257
+ ],
258
+ },
259
+ {
260
+ value: "Tokyo",
261
+ label: "(UTC+09:00) Tokyo",
262
+ offset: "+09:00",
263
+ iana: ["Asia/Tokyo", "Asia/Seoul", "Asia/Yakutsk"],
264
+ },
265
+ {
266
+ value: "Adelaide",
267
+ label: "(UTC+09:30) Adelaide",
268
+ offset: "+09:30",
269
+ iana: ["Australia/Adelaide", "Australia/Darwin"],
270
+ },
271
+ {
272
+ value: "Sydney",
273
+ label: "(UTC+10:00) Sydney",
274
+ offset: "+10:00",
275
+ iana: [
276
+ "Australia/Sydney",
277
+ "Australia/Melbourne",
278
+ "Australia/Brisbane",
279
+ "Australia/Hobart",
280
+ "Pacific/Guam",
281
+ "Asia/Vladivostok",
282
+ ],
283
+ },
284
+ {
285
+ value: "Noumea",
286
+ label: "(UTC+11:00) Noumea",
287
+ offset: "+11:00",
288
+ iana: ["Pacific/Noumea", "Asia/Magadan", "Pacific/Guadalcanal"],
289
+ },
290
+ {
291
+ value: "Auckland",
292
+ label: "(UTC+12:00) Auckland",
293
+ offset: "+12:00",
294
+ iana: ["Pacific/Auckland", "Pacific/Fiji", "Asia/Kamchatka"],
295
+ },
296
+ {
297
+ value: "Nuku'alofa",
298
+ label: "(UTC+13:00) Nuku'alofa",
299
+ offset: "+13:00",
300
+ iana: ["Pacific/Tongatapu", "Pacific/Apia"],
301
+ },
302
+ ];
303
+
304
+ /**
305
+ * Maps an IANA timezone name (e.g. "Asia/Shanghai") to our timezone list value
306
+ * (e.g. "Beijing"). Returns "UTC" if no match found.
307
+ *
308
+ * @param iana - IANA timezone identifier from Intl.DateTimeFormat
309
+ * @returns The matching timezone value from TIMEZONES
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * mapIanaToTimezone("Asia/Shanghai"); // "Beijing"
314
+ * mapIanaToTimezone("America/New_York"); // "Eastern Time (US & Canada)"
315
+ * mapIanaToTimezone("Unknown/Zone"); // "UTC"
316
+ * ```
317
+ */
318
+ export function mapIanaToTimezone(iana: string): string {
319
+ for (const tz of TIMEZONES) {
320
+ if (tz.iana.includes(iana)) {
321
+ return tz.value;
322
+ }
323
+ }
324
+ return "UTC";
325
+ }
package/src/lib/view.ts CHANGED
@@ -77,7 +77,7 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
77
77
  ctx.r2PublicUrl,
78
78
  ctx.s3PublicUrl,
79
79
  );
80
- const url = getMediaUrl(media.id, media.storageKey, publicUrl);
80
+ const url = getMediaUrl(media.storageKey, publicUrl);
81
81
  const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
82
82
  width: 400,
83
83
  quality: 80,
@@ -109,7 +109,7 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
109
109
  * @returns Render-ready PostView with pre-computed fields
110
110
  */
111
111
  export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
112
- const permalink = post.slug ? `/${post.slug}` : `/p/${encode(post.id)}`;
112
+ const permalink = post.path ? `/${post.path}` : `/p/${encode(post.id)}`;
113
113
 
114
114
  // Pre-compute excerpt from raw body
115
115
  let excerpt: string | undefined;
@@ -141,7 +141,7 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
141
141
  return {
142
142
  id: post.id,
143
143
  permalink,
144
- slug: post.slug ?? undefined,
144
+ path: post.path ?? undefined,
145
145
  title: post.title ?? undefined,
146
146
  bodyHtml: post.bodyHtml ?? undefined,
147
147
  excerpt,
package/src/preset.css CHANGED
@@ -10,7 +10,8 @@
10
10
  @import "basecoat-css";
11
11
  @plugin "@tailwindcss/typography";
12
12
  @import "./styles/components.css";
13
- @import "./themes/threads/style.css";
13
+ @import "./styles/tokens.css";
14
+ @import "./styles/ui.css";
14
15
 
15
16
  @theme {
16
17
  --radius-default: 0.5rem;
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createTestApp } from "../../__tests__/helpers/app.js";
3
+ import { composeRoutes } from "../compose.js";
4
+
5
+ describe("Compose Routes", () => {
6
+ describe("POST /compose", () => {
7
+ it("redirects to signin when not authenticated", async () => {
8
+ const { app } = createTestApp({ authenticated: false });
9
+ app.route("/compose", composeRoutes);
10
+
11
+ const res = await app.request("/compose", {
12
+ method: "POST",
13
+ headers: { "Content-Type": "application/json" },
14
+ body: JSON.stringify({ format: "note", body: "Hello" }),
15
+ });
16
+
17
+ expect(res.status).toBe(302);
18
+ expect(res.headers.get("Location")).toBe("/signin");
19
+ });
20
+
21
+ it("creates a note post and returns redirect", async () => {
22
+ const { app, services } = createTestApp({ authenticated: true });
23
+ app.route("/compose", composeRoutes);
24
+
25
+ const res = await app.request("/compose", {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ format: "note", body: "Hello world" }),
29
+ });
30
+
31
+ expect(res.status).toBe(200);
32
+ expect(res.headers.get("Content-Type")).toBe("text/html");
33
+
34
+ // Verify post was created
35
+ const posts = await services.posts.list();
36
+ expect(posts).toHaveLength(1);
37
+ expect(posts[0].format).toBe("note");
38
+ expect(posts[0].body).toBe("Hello world");
39
+ expect(posts[0].status).toBe("published");
40
+ });
41
+
42
+ it("creates a link post", async () => {
43
+ const { app, services } = createTestApp({ authenticated: true });
44
+ app.route("/compose", composeRoutes);
45
+
46
+ const res = await app.request("/compose", {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({
50
+ format: "link",
51
+ body: "Check this out",
52
+ url: "https://example.com",
53
+ }),
54
+ });
55
+
56
+ expect(res.status).toBe(200);
57
+
58
+ const posts = await services.posts.list();
59
+ expect(posts).toHaveLength(1);
60
+ expect(posts[0].format).toBe("link");
61
+ expect(posts[0].url).toBe("https://example.com");
62
+ });
63
+
64
+ it("creates a quote post", async () => {
65
+ const { app, services } = createTestApp({ authenticated: true });
66
+ app.route("/compose", composeRoutes);
67
+
68
+ const res = await app.request("/compose", {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ format: "quote",
73
+ body: "Great insight",
74
+ quoteText: "The original quote",
75
+ url: "https://example.com/source",
76
+ }),
77
+ });
78
+
79
+ expect(res.status).toBe(200);
80
+
81
+ const posts = await services.posts.list();
82
+ expect(posts).toHaveLength(1);
83
+ expect(posts[0].format).toBe("quote");
84
+ expect(posts[0].quoteText).toBe("The original quote");
85
+ });
86
+
87
+ it("creates a draft when status is draft", async () => {
88
+ const { app, services } = createTestApp({ authenticated: true });
89
+ app.route("/compose", composeRoutes);
90
+
91
+ const res = await app.request("/compose", {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({
95
+ format: "note",
96
+ body: "Draft content",
97
+ status: "draft",
98
+ }),
99
+ });
100
+
101
+ expect(res.status).toBe(200);
102
+
103
+ const posts = await services.posts.list({ includeDrafts: true });
104
+ expect(posts).toHaveLength(1);
105
+ expect(posts[0].status).toBe("draft");
106
+ });
107
+
108
+ it("returns error for invalid format", async () => {
109
+ const { app } = createTestApp({ authenticated: true });
110
+ app.route("/compose", composeRoutes);
111
+
112
+ const res = await app.request("/compose", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ format: "invalid", body: "Hello" }),
116
+ });
117
+
118
+ expect(res.status).toBe(200);
119
+ expect(res.headers.get("Content-Type")).toBe("text/html");
120
+ // Returns a toast error (text/html with error message)
121
+ const text = await res.text();
122
+ expect(text).toContain("toast-error");
123
+ });
124
+
125
+ it("attaches media IDs when provided", async () => {
126
+ const { app, services } = createTestApp({ authenticated: true });
127
+ app.route("/compose", composeRoutes);
128
+
129
+ // Create media first
130
+ const media = await services.media.create({
131
+ filename: "test.jpg",
132
+ originalName: "test.jpg",
133
+ mimeType: "image/jpeg",
134
+ size: 1024,
135
+ storageKey: "media/2025/01/test.jpg",
136
+ width: 800,
137
+ height: 600,
138
+ });
139
+
140
+ const res = await app.request("/compose", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({
144
+ format: "note",
145
+ body: "Post with media",
146
+ mediaIds: [media.id],
147
+ }),
148
+ });
149
+
150
+ expect(res.status).toBe(200);
151
+
152
+ const posts = await services.posts.list();
153
+ expect(posts).toHaveLength(1);
154
+
155
+ // Verify media is attached
156
+ const attachments = await services.media.getByPostId(posts[0].id);
157
+ expect(attachments).toHaveLength(1);
158
+ expect(attachments[0].id).toBe(media.id);
159
+ });
160
+
161
+ it("sets featured and pinned flags", async () => {
162
+ const { app, services } = createTestApp({ authenticated: true });
163
+ app.route("/compose", composeRoutes);
164
+
165
+ const res = await app.request("/compose", {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({
169
+ format: "note",
170
+ body: "Featured and pinned",
171
+ featured: true,
172
+ pinned: true,
173
+ }),
174
+ });
175
+
176
+ expect(res.status).toBe(200);
177
+
178
+ const posts = await services.posts.list();
179
+ expect(posts).toHaveLength(1);
180
+ expect(posts[0].featured).toBe(1);
181
+ expect(posts[0].pinned).toBe(1);
182
+ });
183
+
184
+ it("returns error when format is missing", async () => {
185
+ const { app } = createTestApp({ authenticated: true });
186
+ app.route("/compose", composeRoutes);
187
+
188
+ const res = await app.request("/compose", {
189
+ method: "POST",
190
+ headers: { "Content-Type": "application/json" },
191
+ body: JSON.stringify({ body: "No format" }),
192
+ });
193
+
194
+ expect(res.status).toBe(200);
195
+ const text = await res.text();
196
+ expect(text).toContain("toast-error");
197
+ });
198
+ });
199
+ });