@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
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Compose Fullscreen (Zen Mode)
3
+ *
4
+ * Full-screen overlay editor with its own Tiptap instance.
5
+ * Opens from compose editor via jant:fullscreen-open event,
6
+ * returns content via jant:fullscreen-close event.
7
+ *
8
+ * Light DOM only — BaseCoat and Tailwind classes apply directly.
9
+ */
10
+
11
+ import { LitElement, html, nothing } from "lit";
12
+ import type { Editor, JSONContent } from "@tiptap/core";
13
+ import type { ComposeLabels } from "./compose-types.js";
14
+ import { createTiptapEditor } from "../tiptap/create-editor.js";
15
+ import { uploadWithMetadata } from "../upload-with-metadata.js";
16
+
17
+ export class JantComposeFullscreen extends LitElement {
18
+ static properties = {
19
+ labels: { type: Object },
20
+ _open: { state: true },
21
+ _title: { state: true },
22
+ _showTitle: { state: true },
23
+ };
24
+
25
+ declare labels: ComposeLabels;
26
+ declare _open: boolean;
27
+ declare _title: string;
28
+ declare _showTitle: boolean;
29
+
30
+ private _editor: Editor | null = null;
31
+ private _content: JSONContent | null = null;
32
+ private _fileInput: HTMLInputElement | null = null;
33
+
34
+ createRenderRoot() {
35
+ return this;
36
+ }
37
+
38
+ constructor() {
39
+ super();
40
+ this.labels = {} as ComposeLabels;
41
+ this._open = false;
42
+ this._title = "";
43
+ this._showTitle = false;
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ document.addEventListener(
49
+ "jant:fullscreen-open",
50
+ this._onOpen as EventListener,
51
+ );
52
+ document.addEventListener("jant:slash-image", this._onSlashImage);
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ super.disconnectedCallback();
57
+ document.removeEventListener(
58
+ "jant:fullscreen-open",
59
+ this._onOpen as EventListener,
60
+ );
61
+ document.removeEventListener("jant:slash-image", this._onSlashImage);
62
+ this._fileInput?.remove();
63
+ this._destroyEditor();
64
+ }
65
+
66
+ private _onSlashImage = () => {
67
+ if (!this._open || !this._editor) return;
68
+ this._triggerImagePicker();
69
+ };
70
+
71
+ private _triggerImagePicker() {
72
+ if (!this._fileInput) {
73
+ this._fileInput = document.createElement("input");
74
+ this._fileInput.type = "file";
75
+ this._fileInput.accept = "image/*";
76
+ this._fileInput.style.display = "none";
77
+ this._fileInput.addEventListener("change", () => {
78
+ const file = this._fileInput?.files?.[0];
79
+ if (file && this._editor) {
80
+ this._uploadAndInsertImage(file);
81
+ }
82
+ if (this._fileInput) this._fileInput.value = "";
83
+ });
84
+ document.body.appendChild(this._fileInput);
85
+ }
86
+ this._fileInput.click();
87
+ }
88
+
89
+ private async _uploadAndInsertImage(file: File) {
90
+ if (!this._editor) return;
91
+
92
+ const placeholderUrl = URL.createObjectURL(file);
93
+ this._editor.chain().focus().setImage({ src: placeholderUrl }).run();
94
+
95
+ try {
96
+ const data = await uploadWithMetadata(file);
97
+
98
+ // Replace placeholder with real URL
99
+ const { doc } = this._editor.state;
100
+ let replaced = false;
101
+ doc.descendants((node, pos) => {
102
+ if (
103
+ replaced ||
104
+ node.type.name !== "image" ||
105
+ node.attrs.src !== placeholderUrl
106
+ )
107
+ return;
108
+ this._editor
109
+ ?.chain()
110
+ .focus()
111
+ .command(({ tr }) => {
112
+ tr.setNodeMarkup(pos, undefined, { ...node.attrs, src: data.url });
113
+ return true;
114
+ })
115
+ .run();
116
+ replaced = true;
117
+ });
118
+ } catch {
119
+ // Remove placeholder on failure
120
+ const { doc } = this._editor.state;
121
+ doc.descendants((node, pos) => {
122
+ if (node.type.name === "image" && node.attrs.src === placeholderUrl) {
123
+ this._editor
124
+ ?.chain()
125
+ .command(({ tr }) => {
126
+ tr.delete(pos, pos + node.nodeSize);
127
+ return true;
128
+ })
129
+ .run();
130
+ }
131
+ });
132
+ } finally {
133
+ URL.revokeObjectURL(placeholderUrl);
134
+ }
135
+ }
136
+
137
+ private _onOpen = (
138
+ e: CustomEvent<{
139
+ json: JSONContent | null;
140
+ title: string;
141
+ showTitle: boolean;
142
+ format?: string;
143
+ labels?: ComposeLabels;
144
+ }>,
145
+ ) => {
146
+ this._content = e.detail.json;
147
+ this._title = e.detail.title;
148
+ if (e.detail.labels) {
149
+ this.labels = e.detail.labels;
150
+ }
151
+ // Always show title in fullscreen — it's the primary editing surface
152
+ this._showTitle = true;
153
+ this._open = true;
154
+ // Show as modal (top layer) and init editor after render
155
+ this.updateComplete.then(() => {
156
+ const dialog = this.querySelector<HTMLDialogElement>(
157
+ ".compose-fullscreen-dialog",
158
+ );
159
+ if (dialog && !dialog.open) {
160
+ dialog.showModal();
161
+ }
162
+ this._initEditor();
163
+ });
164
+ };
165
+
166
+ private _initEditor() {
167
+ const container = this.querySelector<HTMLElement>(
168
+ ".compose-fullscreen .compose-tiptap-body",
169
+ );
170
+ if (!container || this._editor) return;
171
+
172
+ this._editor = createTiptapEditor({
173
+ element: container,
174
+ placeholder: this.labels.bodyPlaceholder ?? "Write something…",
175
+ content: this._content,
176
+ onUpdate: (json) => {
177
+ this._content = json;
178
+ },
179
+ });
180
+ }
181
+
182
+ private _destroyEditor() {
183
+ this._editor?.destroy();
184
+ this._editor = null;
185
+ }
186
+
187
+ private _onDialogCancel = (e: Event) => {
188
+ // Intercept Escape key to save content back instead of just closing
189
+ e.preventDefault();
190
+ this._close();
191
+ };
192
+
193
+ private _close() {
194
+ const json = this._editor?.getJSON() ?? this._content;
195
+ this._destroyEditor();
196
+
197
+ // Close the modal dialog before Lit removes it from DOM
198
+ const dialog = this.querySelector<HTMLDialogElement>(
199
+ ".compose-fullscreen-dialog",
200
+ );
201
+ dialog?.close();
202
+ this._open = false;
203
+
204
+ // Dispatch on document so the compose dialog (a separate subtree) receives it
205
+ document.dispatchEvent(
206
+ new CustomEvent("jant:fullscreen-close", {
207
+ bubbles: true,
208
+ detail: { json, title: this._title },
209
+ }),
210
+ );
211
+ }
212
+
213
+ /** Insert "/" at cursor to trigger the slash command popup */
214
+ private _insertSlash() {
215
+ if (!this._editor) return;
216
+ this._editor.chain().focus().insertContent("/").run();
217
+ }
218
+
219
+ render() {
220
+ if (!this._open) return nothing;
221
+
222
+ return html`
223
+ <dialog class="compose-fullscreen-dialog" @cancel=${this._onDialogCancel}>
224
+ <div class="compose-fullscreen">
225
+ <div class="compose-fullscreen-toolbar">
226
+ <button
227
+ type="button"
228
+ class="compose-tool-btn"
229
+ @click=${() => this._insertSlash()}
230
+ >
231
+ <svg
232
+ width="18"
233
+ height="18"
234
+ viewBox="0 0 18 18"
235
+ fill="none"
236
+ stroke="currentColor"
237
+ stroke-width="2"
238
+ stroke-linecap="round"
239
+ >
240
+ <line x1="9" y1="3" x2="9" y2="15" />
241
+ <line x1="3" y1="9" x2="15" y2="9" />
242
+ </svg>
243
+ </button>
244
+ <div class="flex-1"></div>
245
+ <button
246
+ type="button"
247
+ class="compose-tool-btn"
248
+ @click=${() => this._close()}
249
+ >
250
+ ${this.labels.done || "Done"}
251
+ </button>
252
+ </div>
253
+ <div class="compose-fullscreen-content">
254
+ <div class="compose-fullscreen-inner">
255
+ ${this._showTitle
256
+ ? html`
257
+ <input
258
+ type="text"
259
+ .value=${this._title}
260
+ @input=${(e: Event) => {
261
+ this._title = (e.target as HTMLInputElement).value;
262
+ }}
263
+ @keydown=${(e: globalThis.KeyboardEvent) => {
264
+ if (e.key === "Enter") {
265
+ e.preventDefault();
266
+ this._editor?.commands.focus("start");
267
+ }
268
+ }}
269
+ class="compose-fullscreen-title"
270
+ placeholder=${this.labels.titlePlaceholder ?? "Title"}
271
+ />
272
+ `
273
+ : nothing}
274
+ <div class="compose-tiptap-body"></div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </dialog>
279
+ `;
280
+ }
281
+ }
282
+
283
+ customElements.define("jant-compose-fullscreen", JantComposeFullscreen);
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Media Lightbox
3
+ *
4
+ * Fullscreen overlay carousel for post media galleries.
5
+ * Intercepts clicks on [data-post-media] a[data-lightbox-index] via
6
+ * delegated listener, reads image data from [data-lightbox-group],
7
+ * and displays images in a native <dialog>.
8
+ *
9
+ * Light DOM only — BaseCoat and Tailwind classes apply directly.
10
+ */
11
+
12
+ import { LitElement, html, nothing } from "lit";
13
+
14
+ interface LightboxImage {
15
+ url: string;
16
+ alt: string;
17
+ width?: number;
18
+ height?: number;
19
+ mimeType?: string;
20
+ posterUrl?: string;
21
+ }
22
+
23
+ export class JantMediaLightbox extends LitElement {
24
+ static properties = {
25
+ _images: { state: true },
26
+ _currentIndex: { state: true },
27
+ _open: { state: true },
28
+ };
29
+
30
+ declare _images: LightboxImage[];
31
+ declare _currentIndex: number;
32
+ declare _open: boolean;
33
+
34
+ createRenderRoot() {
35
+ this.innerHTML = "";
36
+ return this;
37
+ }
38
+
39
+ constructor() {
40
+ super();
41
+ this._images = [];
42
+ this._currentIndex = 0;
43
+ this._open = false;
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback();
48
+ document.addEventListener("click", this.#handleDocumentClick);
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ super.disconnectedCallback();
53
+ document.removeEventListener("click", this.#handleDocumentClick);
54
+ }
55
+
56
+ open(images: LightboxImage[], index: number) {
57
+ this._images = images;
58
+ this._currentIndex = Math.max(0, Math.min(index, images.length - 1));
59
+ this._open = true;
60
+ this.updateComplete.then(() => {
61
+ const dialog = this.querySelector<HTMLDialogElement>(".media-lightbox");
62
+ dialog?.showModal();
63
+ // Focus the content wrapper instead of letting the browser auto-focus
64
+ // the close button, which would show a focus ring on arrow-key nav.
65
+ this.querySelector<HTMLElement>(".media-lightbox-content")?.focus();
66
+ });
67
+ }
68
+
69
+ close() {
70
+ this.querySelector<HTMLDialogElement>(".media-lightbox")?.close();
71
+ this._open = false;
72
+ }
73
+
74
+ #handleDocumentClick = (e: Event) => {
75
+ const target = e.target as HTMLElement;
76
+
77
+ // Find the closest anchor with data-lightbox-index inside [data-post-media]
78
+ // Media gallery lightbox (existing)
79
+ const anchor = target.closest<HTMLAnchorElement>(
80
+ "[data-post-media] a[data-lightbox-index]",
81
+ );
82
+ if (anchor) {
83
+ const group = anchor.closest<HTMLElement>("[data-lightbox-group]");
84
+ if (!group) return;
85
+
86
+ e.preventDefault();
87
+
88
+ const index = parseInt(anchor.dataset.lightboxIndex ?? "0", 10);
89
+ try {
90
+ const images: LightboxImage[] = JSON.parse(
91
+ group.dataset.lightboxGroup ?? "[]",
92
+ );
93
+ if (images.length > 0) {
94
+ this.open(images, index);
95
+ }
96
+ } catch {
97
+ // JSON parse failed — fall through to default link behavior
98
+ }
99
+ return;
100
+ }
101
+
102
+ // Inline body images — collect all <img> within the same [data-post-body]
103
+ const img = target.closest<HTMLImageElement>("[data-post-body] img");
104
+ if (img) {
105
+ e.preventDefault();
106
+ const container = img.closest<HTMLElement>("[data-post-body]");
107
+ if (!container) return;
108
+ const allImages = Array.from(
109
+ container.querySelectorAll<HTMLImageElement>("img"),
110
+ );
111
+ const images: LightboxImage[] = allImages.map((i) => ({
112
+ url: i.src,
113
+ alt: i.alt || "",
114
+ }));
115
+ const index = allImages.indexOf(img);
116
+ if (images.length > 0) this.open(images, Math.max(0, index));
117
+ }
118
+ };
119
+
120
+ #prev() {
121
+ if (this._images.length <= 1) return;
122
+ this._currentIndex =
123
+ (this._currentIndex - 1 + this._images.length) % this._images.length;
124
+ }
125
+
126
+ #next() {
127
+ if (this._images.length <= 1) return;
128
+ this._currentIndex = (this._currentIndex + 1) % this._images.length;
129
+ }
130
+
131
+ #handleKeydown = (e: Event) => {
132
+ const ke = e as globalThis.KeyboardEvent;
133
+ if (ke.key === "ArrowLeft") {
134
+ e.preventDefault();
135
+ this.#prev();
136
+ } else if (ke.key === "ArrowRight") {
137
+ e.preventDefault();
138
+ this.#next();
139
+ }
140
+ };
141
+
142
+ #handleDialogClick = (e: Event) => {
143
+ const target = e.target as HTMLElement;
144
+ // Close on backdrop click (dialog itself or the content wrapper, not media/buttons)
145
+ if (
146
+ target === e.currentTarget ||
147
+ target.classList.contains("media-lightbox-content")
148
+ ) {
149
+ this.close();
150
+ }
151
+ };
152
+
153
+ #handleClose = () => {
154
+ this._open = false;
155
+ };
156
+
157
+ render() {
158
+ if (!this._open) return nothing;
159
+
160
+ const img = this._images[this._currentIndex];
161
+ const multiple = this._images.length > 1;
162
+
163
+ return html`
164
+ <dialog
165
+ class="media-lightbox"
166
+ @keydown=${this.#handleKeydown}
167
+ @click=${this.#handleDialogClick}
168
+ @close=${this.#handleClose}
169
+ >
170
+ <div class="media-lightbox-content" tabindex="-1">
171
+ <button
172
+ type="button"
173
+ class="media-lightbox-close"
174
+ @click=${() => this.close()}
175
+ aria-label="Close"
176
+ >
177
+ <svg
178
+ width="20"
179
+ height="20"
180
+ viewBox="0 0 24 24"
181
+ fill="none"
182
+ stroke="currentColor"
183
+ stroke-width="2"
184
+ stroke-linecap="round"
185
+ stroke-linejoin="round"
186
+ >
187
+ <path d="M18 6 6 18" />
188
+ <path d="m6 6 12 12" />
189
+ </svg>
190
+ </button>
191
+
192
+ ${multiple
193
+ ? html`<div class="media-lightbox-counter">
194
+ ${this._currentIndex + 1} / ${this._images.length}
195
+ </div>`
196
+ : nothing}
197
+ ${img?.mimeType?.startsWith("video/")
198
+ ? html`<video
199
+ class="media-lightbox-video"
200
+ src=${img.url}
201
+ poster=${img.posterUrl ?? ""}
202
+ controls
203
+ autoplay
204
+ playsinline
205
+ ></video>`
206
+ : html`<img
207
+ class="media-lightbox-img"
208
+ src=${img?.url ?? ""}
209
+ alt=${img?.alt ?? ""}
210
+ />`}
211
+ ${multiple
212
+ ? html`
213
+ <button
214
+ type="button"
215
+ class="media-lightbox-nav media-lightbox-nav-prev"
216
+ @click=${() => this.#prev()}
217
+ aria-label="Previous"
218
+ >
219
+ <svg
220
+ width="24"
221
+ height="24"
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ stroke-width="2"
226
+ stroke-linecap="round"
227
+ stroke-linejoin="round"
228
+ >
229
+ <path d="m15 18-6-6 6-6" />
230
+ </svg>
231
+ </button>
232
+ <button
233
+ type="button"
234
+ class="media-lightbox-nav media-lightbox-nav-next"
235
+ @click=${() => this.#next()}
236
+ aria-label="Next"
237
+ >
238
+ <svg
239
+ width="24"
240
+ height="24"
241
+ viewBox="0 0 24 24"
242
+ fill="none"
243
+ stroke="currentColor"
244
+ stroke-width="2"
245
+ stroke-linecap="round"
246
+ stroke-linejoin="round"
247
+ >
248
+ <path d="m9 18 6-6-6-6" />
249
+ </svg>
250
+ </button>
251
+ `
252
+ : nothing}
253
+ </div>
254
+ </dialog>
255
+ `;
256
+ }
257
+ }
258
+
259
+ customElements.define("jant-media-lightbox", JantMediaLightbox);