@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,97 +0,0 @@
1
- /**
2
- * Redirect Service
3
- *
4
- * URL redirect management for path changes
5
- */
6
-
7
- import { eq } from "drizzle-orm";
8
- import type { Database } from "../db/index.js";
9
- import { redirects } from "../db/schema.js";
10
- import { now } from "../lib/time.js";
11
- import { normalizePath } from "../lib/url.js";
12
- import type { Redirect } from "../types.js";
13
- import type { PathRegistryService } from "./path-registry.js";
14
- import { ConflictError } from "../lib/errors.js";
15
-
16
- export interface RedirectService {
17
- getByPath(fromPath: string): Promise<Redirect | null>;
18
- create(fromPath: string, toPath: string, type?: 301 | 302): Promise<Redirect>;
19
- delete(id: number): Promise<boolean>;
20
- list(): Promise<Redirect[]>;
21
- }
22
-
23
- export function createRedirectService(
24
- db: Database,
25
- pathRegistry: PathRegistryService,
26
- ): RedirectService {
27
- function toRedirect(row: typeof redirects.$inferSelect): Redirect {
28
- return {
29
- id: row.id,
30
- fromPath: row.fromPath,
31
- toPath: row.toPath,
32
- type: row.type as 301 | 302,
33
- createdAt: row.createdAt,
34
- };
35
- }
36
-
37
- return {
38
- async getByPath(fromPath) {
39
- const normalized = normalizePath(fromPath);
40
- const result = await db
41
- .select()
42
- .from(redirects)
43
- .where(eq(redirects.fromPath, normalized))
44
- .limit(1);
45
- return result[0] ? toRedirect(result[0]) : null;
46
- },
47
-
48
- async create(fromPath, toPath, type = 301) {
49
- const timestamp = now();
50
- const normalizedFrom = normalizePath(fromPath);
51
-
52
- // Check if path is claimed by a non-redirect entity
53
- const existingClaim = await pathRegistry.getByPath(normalizedFrom);
54
- if (existingClaim && existingClaim.ownerType !== "redirect") {
55
- throw new ConflictError(`Path "${normalizedFrom}" is already in use`);
56
- }
57
-
58
- // Delete existing redirect from this path if any (upsert behavior)
59
- if (existingClaim?.ownerType === "redirect") {
60
- await pathRegistry.release(normalizedFrom);
61
- }
62
- await db.delete(redirects).where(eq(redirects.fromPath, normalizedFrom));
63
-
64
- const result = await db
65
- .insert(redirects)
66
- .values({
67
- fromPath: normalizedFrom,
68
- toPath,
69
- type,
70
- createdAt: timestamp,
71
- })
72
- .returning();
73
-
74
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
75
- const redirect = toRedirect(result[0]!);
76
-
77
- await pathRegistry.claim(normalizedFrom, "redirect", redirect.id);
78
-
79
- return redirect;
80
- },
81
-
82
- async delete(id) {
83
- // Release path registry entries for this redirect
84
- await pathRegistry.releaseByOwner("redirect", id);
85
- const result = await db
86
- .delete(redirects)
87
- .where(eq(redirects.id, id))
88
- .returning();
89
- return result.length > 0;
90
- },
91
-
92
- async list() {
93
- const rows = await db.select().from(redirects);
94
- return rows.map(toRedirect);
95
- },
96
- };
97
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Minimal type declarations for sortablejs
3
- *
4
- * Only covers the API surface used by jant-nav-manager and collections-reorder.
5
- */
6
-
7
- declare module "sortablejs" {
8
- interface SortableEvent {
9
- oldIndex?: number;
10
- newIndex?: number;
11
- item: HTMLElement;
12
- }
13
-
14
- interface SortableOptions {
15
- animation?: number;
16
- handle?: string;
17
- onEnd?: (event: SortableEvent) => void;
18
- }
19
-
20
- interface SortableInstance {
21
- destroy(): void;
22
- }
23
-
24
- const Sortable: {
25
- create(el: HTMLElement, options?: SortableOptions): SortableInstance;
26
- };
27
-
28
- export default Sortable;
29
- }
@@ -1,512 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import { describe, it, expect, beforeEach } from "vitest";
4
- import type {
5
- ComposeLabels,
6
- ComposeCollection,
7
- ComposeSubmitDetail,
8
- } from "../compose-types.js";
9
- import "../jant-compose-editor.js";
10
- import "../jant-compose-dialog.js";
11
- import type { JantComposeDialog } from "../jant-compose-dialog.js";
12
- import type { JantComposeEditor } from "../jant-compose-editor.js";
13
-
14
- function requireElement<T extends globalThis.Element>(
15
- element: T | null,
16
- message: string,
17
- ): T {
18
- if (!element) {
19
- throw new Error(message);
20
- }
21
- return element;
22
- }
23
-
24
- const labels: ComposeLabels = {
25
- cancel: "Cancel",
26
- note: "Note",
27
- link: "Link",
28
- quote: "Quote",
29
- saveDraft: "Save as Draft",
30
- saveAsDraft: "Save as draft",
31
- discard: "Discard",
32
- titlePlaceholder: "Title",
33
- bodyPlaceholder: "What's on your mind...",
34
- urlPlaceholder: "Paste a URL...",
35
- linkTitlePlaceholder: "Give it a title...",
36
- thoughtsPlaceholder: "Your thoughts (optional)",
37
- quotePlaceholder: "Type the quote...",
38
- authorPlaceholder: "Author (optional)",
39
- sourcePlaceholder: "Source link (optional)",
40
- attachedText: "Attached Text",
41
- attachedTextPlaceholder: "Paste text...",
42
- attachedTextHint: "Supplementary content",
43
- done: "Done",
44
- media: "Media",
45
- score: "Score",
46
- title: "Title",
47
- collection: "Collection",
48
- searchCollections: "Search...",
49
- noCollections: "No collections found.",
50
- post: "Post",
51
- addAlt: "+ ALT",
52
- addAltTitle: "Add alt text",
53
- altPlaceholder: "Describe this...",
54
- altHint: "Alt text improves accessibility",
55
- addMore: "Add",
56
- uploading: "Uploading...",
57
- published: "Published!",
58
- };
59
-
60
- const collections: ComposeCollection[] = [
61
- { id: 1, title: "Books", iconHtml: "" },
62
- { id: 2, title: "Movies", iconHtml: "<span>🎬</span>" },
63
- ];
64
-
65
- async function createElement(
66
- cols: ComposeCollection[] = collections,
67
- ): Promise<JantComposeDialog> {
68
- const el = document.createElement("jant-compose-dialog") as JantComposeDialog;
69
- el.collections = cols;
70
- el.labels = labels;
71
- document.body.appendChild(el);
72
- await el.updateComplete;
73
- // Wait for nested editor to also render
74
- const editor = el.querySelector<JantComposeEditor>("jant-compose-editor");
75
- if (editor) await editor.updateComplete;
76
- return el;
77
- }
78
-
79
- describe("JantComposeDialog", () => {
80
- beforeEach(() => {
81
- document.body.innerHTML = "";
82
- });
83
-
84
- it("renders with collections and labels", async () => {
85
- const el = await createElement();
86
-
87
- // Header present
88
- expect(el.querySelector(".compose-dialog-header")).not.toBeNull();
89
-
90
- // Format buttons present
91
- const segmentedItems = el.querySelectorAll(".compose-segmented-item");
92
- expect(segmentedItems.length).toBe(3);
93
- expect(segmentedItems[0].textContent?.trim()).toBe("Note");
94
- expect(segmentedItems[1].textContent?.trim()).toBe("Link");
95
- expect(segmentedItems[2].textContent?.trim()).toBe("Quote");
96
-
97
- // Post button present
98
- const postBtn = requireElement(
99
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
100
- "expected post button",
101
- );
102
- expect(postBtn.textContent?.trim()).toBe("Post");
103
- });
104
-
105
- it("format switching updates active state", async () => {
106
- const el = await createElement();
107
-
108
- // Note is active by default
109
- const noteBtn = el.querySelectorAll<HTMLButtonElement>(
110
- ".compose-segmented-item",
111
- )[0];
112
- expect(noteBtn.classList.contains("compose-segmented-item-active")).toBe(
113
- true,
114
- );
115
-
116
- // Click link
117
- const linkBtn = el.querySelectorAll<HTMLButtonElement>(
118
- ".compose-segmented-item",
119
- )[1];
120
- linkBtn.click();
121
- await el.updateComplete;
122
-
123
- expect(el._format).toBe("link");
124
- expect(linkBtn.classList.contains("compose-segmented-item-active")).toBe(
125
- true,
126
- );
127
- expect(noteBtn.classList.contains("compose-segmented-item-active")).toBe(
128
- false,
129
- );
130
- });
131
-
132
- it("submit dispatches jant:compose-submit with correct payload", async () => {
133
- const el = await createElement();
134
- const editor = requireElement(
135
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
136
- "expected compose editor",
137
- );
138
- editor._body = "Hello world";
139
- await editor.updateComplete;
140
-
141
- let receivedDetail: ComposeSubmitDetail | null = null;
142
- el.addEventListener("jant:compose-submit", (event) => {
143
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
144
- receivedDetail = customEvent.detail;
145
- });
146
-
147
- // Click post button
148
- requireElement(
149
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
150
- "expected post button",
151
- ).click();
152
-
153
- expect(receivedDetail).not.toBeNull();
154
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
155
- expect(detail.format).toBe("note");
156
- expect(detail.body).toBe("Hello world");
157
- expect(detail.status).toBe("published");
158
- expect(detail.collectionIds).toEqual([]);
159
- expect(detail.mediaIds).toEqual([]);
160
- expect(detail.mediaAlts).toEqual({});
161
- });
162
-
163
- it("collection selector toggles IDs", async () => {
164
- const el = await createElement();
165
-
166
- // Open collection combobox
167
- const trigger = requireElement(
168
- el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
169
- "expected collection trigger",
170
- );
171
- trigger.click();
172
- await el.updateComplete;
173
-
174
- const options = el.querySelectorAll<HTMLElement>(
175
- "[data-popover] [role='option']",
176
- );
177
- expect(options.length).toBe(2);
178
-
179
- // Select first collection
180
- options[0].click();
181
- await el.updateComplete;
182
- expect(el._collectionIds).toEqual([1]);
183
-
184
- // Select second collection
185
- options[1].click();
186
- await el.updateComplete;
187
- expect(el._collectionIds).toEqual([1, 2]);
188
-
189
- // Deselect first
190
- options[0].click();
191
- await el.updateComplete;
192
- expect(el._collectionIds).toEqual([2]);
193
- });
194
-
195
- it("reset restores initial state", async () => {
196
- const el = await createElement();
197
- el._format = "link";
198
- el._collectionIds = [1, 2];
199
- el._loading = true;
200
-
201
- el.reset();
202
-
203
- expect(el._format).toBe("note");
204
- expect(el._collectionIds).toEqual([]);
205
- expect(el._loading).toBe(false);
206
- });
207
-
208
- it("loading state disables submit button", async () => {
209
- const el = await createElement();
210
- el._loading = true;
211
- await el.updateComplete;
212
-
213
- const postBtn = requireElement(
214
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
215
- "expected post button",
216
- );
217
- expect(postBtn.disabled).toBe(true);
218
- });
219
-
220
- it("renders without collections", async () => {
221
- const el = await createElement([]);
222
-
223
- // No collection trigger
224
- expect(el.querySelector(".compose-collection-trigger")).toBeNull();
225
- // Spacer div present instead
226
- const actionRow = el.querySelector(".compose-action-row");
227
- expect(actionRow).not.toBeNull();
228
- });
229
-
230
- it("draft button dispatches submit with draft status", async () => {
231
- const el = await createElement();
232
- const editor = requireElement(
233
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
234
- "expected compose editor",
235
- );
236
- editor._body = "Draft content";
237
- await editor.updateComplete;
238
-
239
- let receivedDetail: ComposeSubmitDetail | null = null;
240
- el.addEventListener("jant:compose-submit", (event) => {
241
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
242
- receivedDetail = customEvent.detail;
243
- });
244
-
245
- // Click the draft header button
246
- const draftBtn = requireElement(
247
- el.querySelector<HTMLButtonElement>(".compose-dialog-header-btn"),
248
- "expected draft button",
249
- );
250
- draftBtn.click();
251
-
252
- expect(receivedDetail).not.toBeNull();
253
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
254
- expect(detail.status).toBe("draft");
255
- });
256
-
257
- it("does not dispatch submit when loading", async () => {
258
- const el = await createElement();
259
- el._loading = true;
260
- await el.updateComplete;
261
-
262
- let dispatched = false;
263
- el.addEventListener("jant:compose-submit", () => {
264
- dispatched = true;
265
- });
266
-
267
- const postBtn = requireElement(
268
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
269
- "expected post button",
270
- );
271
- postBtn.click();
272
-
273
- expect(dispatched).toBe(false);
274
- });
275
-
276
- it("loading state shows spinner in submit button", async () => {
277
- const el = await createElement();
278
- el._loading = true;
279
- await el.updateComplete;
280
-
281
- const spinner = el.querySelector(".compose-post-btn .animate-spin");
282
- expect(spinner).not.toBeNull();
283
- });
284
-
285
- it("no old media picker dialog is rendered", async () => {
286
- const el = await createElement();
287
-
288
- expect(el.querySelector("#compose-media-picker")).toBeNull();
289
- expect(el.querySelector(".compose-media-picker")).toBeNull();
290
- });
291
-
292
- it("editor renders attachments when present", async () => {
293
- const el = await createElement();
294
- const editor = requireElement(
295
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
296
- "expected compose editor",
297
- );
298
-
299
- // Simulate adding an attachment
300
- const blob = new Blob(["fake-image"], { type: "image/png" });
301
- const file = new File([blob], "test.png", { type: "image/png" });
302
- const previewUrl = URL.createObjectURL(blob);
303
-
304
- editor._attachments = [
305
- {
306
- clientId: "test-id-1",
307
- file,
308
- previewUrl,
309
- status: "done",
310
- mediaId: "media-1",
311
- alt: "",
312
- error: null,
313
- },
314
- ];
315
- await editor.updateComplete;
316
-
317
- // Thumbnail strip should be visible
318
- expect(editor.querySelector(".compose-attachments")).not.toBeNull();
319
- expect(editor.querySelector(".compose-attachment-thumb")).not.toBeNull();
320
- // ALT button should be visible
321
- expect(editor.querySelector(".compose-attachment-alt")).not.toBeNull();
322
- // Media tool button should show "Add" label
323
- const mediaBtn =
324
- editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
325
- expect(mediaBtn?.querySelector(".compose-tool-tip")?.textContent).toBe(
326
- "Add",
327
- );
328
-
329
- URL.revokeObjectURL(previewUrl);
330
- });
331
-
332
- it("remove button clears attachment", async () => {
333
- const el = await createElement();
334
- const editor = requireElement(
335
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
336
- "expected compose editor",
337
- );
338
-
339
- const blob = new Blob(["fake-image"], { type: "image/png" });
340
- const file = new File([blob], "test.png", { type: "image/png" });
341
- const previewUrl = URL.createObjectURL(blob);
342
-
343
- editor._attachments = [
344
- {
345
- clientId: "test-id-1",
346
- file,
347
- previewUrl,
348
- status: "done",
349
- mediaId: "media-1",
350
- alt: "",
351
- error: null,
352
- },
353
- ];
354
- await editor.updateComplete;
355
-
356
- // Click remove button
357
- const removeBtn = requireElement(
358
- editor.querySelector<HTMLButtonElement>(".compose-attachment-remove"),
359
- "expected remove button",
360
- );
361
- removeBtn.click();
362
- await editor.updateComplete;
363
-
364
- // Attachment strip should be gone (no attachments)
365
- expect(editor.querySelector(".compose-attachments")).toBeNull();
366
- expect(editor._attachments.length).toBe(0);
367
- });
368
-
369
- it("alt panel opens and closes", async () => {
370
- const el = await createElement();
371
- const editor = requireElement(
372
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
373
- "expected compose editor",
374
- );
375
-
376
- const blob = new Blob(["fake-image"], { type: "image/png" });
377
- const file = new File([blob], "test.png", { type: "image/png" });
378
- const previewUrl = URL.createObjectURL(blob);
379
-
380
- editor._attachments = [
381
- {
382
- clientId: "test-id-1",
383
- file,
384
- previewUrl,
385
- status: "done",
386
- mediaId: "media-1",
387
- alt: "",
388
- error: null,
389
- },
390
- ];
391
- await editor.updateComplete;
392
-
393
- // Click ALT button
394
- const altBtn = requireElement(
395
- editor.querySelector<HTMLButtonElement>(".compose-attachment-alt"),
396
- "expected alt button",
397
- );
398
- altBtn.click();
399
- await editor.updateComplete;
400
-
401
- // Alt panel should be visible
402
- expect(editor.querySelector(".compose-alt-panel")).not.toBeNull();
403
- expect(editor._showAltPanel).toBe(true);
404
-
405
- // Click done to close
406
- const doneBtn = editor.querySelector<HTMLButtonElement>(
407
- ".compose-alt-panel .compose-post-btn",
408
- );
409
- doneBtn?.click();
410
- await editor.updateComplete;
411
-
412
- expect(editor._showAltPanel).toBe(false);
413
- expect(editor.querySelector(".compose-alt-panel")).toBeNull();
414
-
415
- URL.revokeObjectURL(previewUrl);
416
- });
417
-
418
- it("submit includes mediaIds and mediaAlts from completed attachments", async () => {
419
- const el = await createElement();
420
- const editor = requireElement(
421
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
422
- "expected compose editor",
423
- );
424
-
425
- const blob = new Blob(["fake-image"], { type: "image/png" });
426
- const file = new File([blob], "test.png", { type: "image/png" });
427
- const previewUrl = URL.createObjectURL(blob);
428
-
429
- editor._attachments = [
430
- {
431
- clientId: "test-id-1",
432
- file,
433
- previewUrl,
434
- status: "done",
435
- mediaId: "media-1",
436
- alt: "A test image",
437
- error: null,
438
- },
439
- ];
440
- editor._body = "Post with image";
441
- await editor.updateComplete;
442
-
443
- let receivedDetail: ComposeSubmitDetail | null = null;
444
- el.addEventListener("jant:compose-submit", (event) => {
445
- const customEvent = event as CustomEvent<ComposeSubmitDetail>;
446
- receivedDetail = customEvent.detail;
447
- });
448
-
449
- requireElement(
450
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
451
- "expected post button",
452
- ).click();
453
-
454
- expect(receivedDetail).not.toBeNull();
455
- const detail = receivedDetail as unknown as ComposeSubmitDetail;
456
- expect(detail.mediaIds).toEqual(["media-1"]);
457
- expect(detail.mediaAlts).toEqual({ "media-1": "A test image" });
458
-
459
- URL.revokeObjectURL(previewUrl);
460
- });
461
-
462
- it("dispatches deferred submit when uploads are pending", async () => {
463
- const el = await createElement();
464
- const editor = requireElement(
465
- el.querySelector<JantComposeEditor>("jant-compose-editor"),
466
- "expected compose editor",
467
- );
468
-
469
- const blob = new Blob(["fake-image"], { type: "image/png" });
470
- const file = new File([blob], "test.png", { type: "image/png" });
471
- const previewUrl = URL.createObjectURL(blob);
472
-
473
- editor._attachments = [
474
- {
475
- clientId: "test-id-1",
476
- file,
477
- previewUrl,
478
- status: "uploading",
479
- mediaId: null,
480
- alt: "Alt for pending",
481
- error: null,
482
- },
483
- ];
484
- editor._body = "Post with pending upload";
485
- await editor.updateComplete;
486
-
487
- let deferredEvent: CustomEvent | null = null;
488
- el.addEventListener("jant:compose-submit-deferred", (event) => {
489
- deferredEvent = event as CustomEvent;
490
- });
491
-
492
- // Prevent dialog.close() from throwing (no parent dialog in test)
493
- let submitEvent: CustomEvent | null = null;
494
- el.addEventListener("jant:compose-submit", (event) => {
495
- submitEvent = event as CustomEvent;
496
- });
497
-
498
- requireElement(
499
- el.querySelector<HTMLButtonElement>(".compose-post-btn"),
500
- "expected post button",
501
- ).click();
502
-
503
- // Should have dispatched deferred, not regular submit
504
- expect(deferredEvent).not.toBeNull();
505
- expect(submitEvent).toBeNull();
506
- expect(
507
- (deferredEvent as unknown as CustomEvent).detail.pendingAttachments,
508
- ).toBeDefined();
509
-
510
- URL.revokeObjectURL(previewUrl);
511
- });
512
- });