@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
@@ -87,7 +87,7 @@ export class JantSettingsAvatar extends LitElement {
87
87
  new CustomEvent("jant:settings-save", {
88
88
  bubbles: true,
89
89
  detail: {
90
- endpoint: "/dash/settings/avatar/display",
90
+ endpoint: "/settings/avatar/display",
91
91
  data: { showHeaderAvatar: this._showInHeader ? "true" : "" },
92
92
  section: "avatar-display",
93
93
  },
@@ -101,7 +101,7 @@ export class JantSettingsAvatar extends LitElement {
101
101
  this.dispatchEvent(
102
102
  new CustomEvent("jant:avatar-remove", {
103
103
  bubbles: true,
104
- detail: { endpoint: "/dash/settings/avatar/remove" },
104
+ detail: { endpoint: "/settings/avatar/remove" },
105
105
  }),
106
106
  );
107
107
  }
@@ -146,7 +146,7 @@ export class JantSettingsAvatar extends LitElement {
146
146
  ${this._renderPreview()}
147
147
  <div class="flex flex-col gap-2">
148
148
  <form
149
- action="/dash/settings/avatar"
149
+ action="/settings/avatar"
150
150
  method="post"
151
151
  enctype="multipart/form-data"
152
152
  class="inline"
@@ -170,7 +170,7 @@ export class JantSettingsGeneral extends LitElement {
170
170
  new CustomEvent("jant:settings-save", {
171
171
  bubbles: true,
172
172
  detail: {
173
- endpoint: "/dash/settings",
173
+ endpoint: "/settings/general",
174
174
  data: {
175
175
  siteName: this._siteName,
176
176
  siteDescription: this._siteDescription,
@@ -203,7 +203,7 @@ export class JantSettingsGeneral extends LitElement {
203
203
  new CustomEvent("jant:settings-save", {
204
204
  bubbles: true,
205
205
  detail: {
206
- endpoint: "/dash/settings/seo",
206
+ endpoint: "/settings/general/seo",
207
207
  data: { noindex: this._noindex ? "" : "true" },
208
208
  section: "seo",
209
209
  },
@@ -211,6 +211,24 @@ export class JantSettingsGeneral extends LitElement {
211
211
  );
212
212
  }
213
213
 
214
+ /** Submit on Enter from non-textarea fields */
215
+ private _onKeydown(
216
+ e: globalThis.KeyboardEvent,
217
+ save: () => void,
218
+ dirty: boolean,
219
+ loading: boolean,
220
+ ) {
221
+ if (
222
+ e.key === "Enter" &&
223
+ !loading &&
224
+ dirty &&
225
+ !(e.target instanceof HTMLTextAreaElement)
226
+ ) {
227
+ e.preventDefault();
228
+ save();
229
+ }
230
+ }
231
+
214
232
  // ── Render helpers ────────────────────────────────────────────────
215
233
 
216
234
  private _renderActions(
@@ -258,7 +276,15 @@ export class JantSettingsGeneral extends LitElement {
258
276
 
259
277
  private _renderGeneralForm() {
260
278
  return html`
261
- <div>
279
+ <div
280
+ @keydown=${(e: globalThis.KeyboardEvent) =>
281
+ this._onKeydown(
282
+ e,
283
+ () => this._saveGeneral(),
284
+ this._generalDirty,
285
+ this._generalLoading,
286
+ )}
287
+ >
262
288
  <h2 class="text-lg font-semibold mb-4">${this.labels.general}</h2>
263
289
  <div class="flex flex-col gap-4">
264
290
  <div class="field">
@@ -366,7 +392,15 @@ export class JantSettingsGeneral extends LitElement {
366
392
 
367
393
  private _renderSeoForm() {
368
394
  return html`
369
- <div>
395
+ <div
396
+ @keydown=${(e: globalThis.KeyboardEvent) =>
397
+ this._onKeydown(
398
+ e,
399
+ () => this._saveSeo(),
400
+ this._seoDirty,
401
+ this._seoLoading,
402
+ )}
403
+ >
370
404
  <h2 class="text-lg font-semibold mb-4">${this.labels.seo}</h2>
371
405
  <div>
372
406
  <label class="flex items-center gap-2 cursor-pointer">
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Text Preview Dialog
3
+ *
4
+ * Displays attached text content (TipTap-authored) in a modal dialog.
5
+ * Intercepts clicks on [data-text-preview-url] buttons, fetches the
6
+ * stored { json, html } envelope from the URL, and renders the HTML
7
+ * 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
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
14
+ import { showToast } from "../toast.js";
15
+ import { jsonToMarkdown } from "../tiptap/create-editor.js";
16
+
17
+ export class JantTextPreview extends LitElement {
18
+ static properties = {
19
+ _open: { state: true },
20
+ _html: { state: true },
21
+ _loading: { state: true },
22
+ _copied: { state: true },
23
+ };
24
+
25
+ declare _open: boolean;
26
+ declare _html: string;
27
+ declare _loading: boolean;
28
+ declare _copied: boolean;
29
+ /** Raw text for the copy button (markdown / plain text source) */
30
+ #rawText = "";
31
+
32
+ createRenderRoot() {
33
+ this.innerHTML = "";
34
+ return this;
35
+ }
36
+
37
+ constructor() {
38
+ super();
39
+ this._open = false;
40
+ this._html = "";
41
+ this._loading = false;
42
+ this._copied = false;
43
+ }
44
+
45
+ connectedCallback() {
46
+ super.connectedCallback();
47
+ document.addEventListener("click", this.#handleDocumentClick);
48
+ }
49
+
50
+ disconnectedCallback() {
51
+ super.disconnectedCallback();
52
+ document.removeEventListener("click", this.#handleDocumentClick);
53
+ }
54
+
55
+ #handleDocumentClick = (e: Event) => {
56
+ const target = e.target as HTMLElement;
57
+ const btn = target.closest<HTMLButtonElement>("[data-text-preview-id]");
58
+ if (!btn) return;
59
+
60
+ e.preventDefault();
61
+ const mediaId = btn.dataset.textPreviewId;
62
+ if (mediaId) this.#openPreview(mediaId);
63
+ };
64
+
65
+ async #openPreview(mediaId: string) {
66
+ this._loading = true;
67
+ this._open = true;
68
+
69
+ document.body.style.overflow = "hidden";
70
+
71
+ await this.updateComplete;
72
+ this.querySelector<HTMLDialogElement>(".text-preview-dialog")?.showModal();
73
+
74
+ try {
75
+ const res = await fetch(`/api/media/${mediaId}/content`);
76
+ if (!res.ok) throw new Error("Fetch failed");
77
+
78
+ const raw = await res.text();
79
+
80
+ // Try parsing as { json, html } envelope (TipTap rich text)
81
+ try {
82
+ const envelope = JSON.parse(raw) as {
83
+ json?: import("@tiptap/core").JSONContent;
84
+ html?: string;
85
+ };
86
+ this._html = envelope.html || "";
87
+ // Serialize JSON → markdown via headless TipTap editor
88
+ this.#rawText = envelope.json ? jsonToMarkdown(envelope.json) : "";
89
+ } catch {
90
+ // Not JSON — raw markdown / plain text, copy as-is
91
+ this.#rawText = raw;
92
+ this._html = `<pre>${raw.replace(/</g, "&lt;")}</pre>`;
93
+ }
94
+ } catch {
95
+ this._html = "<p>Failed to load content.</p>";
96
+ this.#rawText = "";
97
+ } finally {
98
+ this._loading = false;
99
+ }
100
+ }
101
+
102
+ #close() {
103
+ this.querySelector<HTMLDialogElement>(".text-preview-dialog")?.close();
104
+ document.body.style.overflow = "";
105
+ this._open = false;
106
+ this._html = "";
107
+ this.#rawText = "";
108
+ this._copied = false;
109
+ }
110
+
111
+ async #copy() {
112
+ if (!this.#rawText) return;
113
+ try {
114
+ await globalThis.navigator.clipboard.writeText(this.#rawText);
115
+ this._copied = true;
116
+ showToast("Copied.");
117
+ setTimeout(() => {
118
+ this._copied = false;
119
+ }, 2000);
120
+ } catch {
121
+ showToast("Could not copy.", "error");
122
+ }
123
+ }
124
+
125
+ #handleKeydown = (e: globalThis.KeyboardEvent) => {
126
+ if (e.key === "Escape") {
127
+ e.preventDefault();
128
+ e.stopPropagation();
129
+ this.#close();
130
+ }
131
+ };
132
+
133
+ render() {
134
+ if (!this._open) return nothing;
135
+
136
+ return html`
137
+ <dialog
138
+ class="text-preview-dialog"
139
+ @cancel=${(e: Event) => {
140
+ e.preventDefault();
141
+ this.#close();
142
+ }}
143
+ @keydown=${this.#handleKeydown}
144
+ @click=${(e: Event) => {
145
+ // Close on backdrop click
146
+ if ((e.target as HTMLElement).tagName === "DIALOG") {
147
+ this.#close();
148
+ }
149
+ }}
150
+ >
151
+ <div class="text-preview-content">
152
+ <div class="text-preview-toolbar">
153
+ <button
154
+ type="button"
155
+ class="text-preview-btn"
156
+ @click=${() => this.#copy()}
157
+ ?disabled=${this._loading || !this.#rawText}
158
+ title="Copy"
159
+ >
160
+ ${this._copied
161
+ ? html`<svg
162
+ width="16"
163
+ height="16"
164
+ viewBox="0 0 24 24"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ stroke-width="2"
168
+ stroke-linecap="round"
169
+ stroke-linejoin="round"
170
+ >
171
+ <path d="M20 6 9 17l-5-5" />
172
+ </svg>`
173
+ : html`<svg
174
+ width="16"
175
+ height="16"
176
+ viewBox="0 0 24 24"
177
+ fill="none"
178
+ stroke="currentColor"
179
+ stroke-width="2"
180
+ stroke-linecap="round"
181
+ stroke-linejoin="round"
182
+ >
183
+ <rect width="14" height="14" x="8" y="8" rx="2" />
184
+ <path
185
+ d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
186
+ />
187
+ </svg>`}
188
+ </button>
189
+ <button
190
+ type="button"
191
+ class="text-preview-btn"
192
+ @click=${() => this.#close()}
193
+ title="Close"
194
+ >
195
+ <svg
196
+ width="16"
197
+ height="16"
198
+ viewBox="0 0 24 24"
199
+ fill="none"
200
+ stroke="currentColor"
201
+ stroke-width="2"
202
+ stroke-linecap="round"
203
+ stroke-linejoin="round"
204
+ >
205
+ <path d="M18 6 6 18M6 6l12 12" />
206
+ </svg>
207
+ </button>
208
+ </div>
209
+ ${this._loading
210
+ ? html`<div class="text-preview-loading">
211
+ <svg
212
+ class="animate-spin size-5"
213
+ viewBox="0 0 24 24"
214
+ fill="none"
215
+ stroke="currentColor"
216
+ stroke-width="2"
217
+ stroke-linecap="round"
218
+ stroke-linejoin="round"
219
+ >
220
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
221
+ </svg>
222
+ </div>`
223
+ : html`<div class="text-preview-body prose">
224
+ ${unsafeHTML(this._html)}
225
+ </div>`}
226
+ </div>
227
+ </dialog>
228
+ `;
229
+ }
230
+ }
231
+
232
+ customElements.define("jant-text-preview", JantTextPreview);
@@ -3,11 +3,10 @@
3
3
  */
4
4
 
5
5
  export interface NavManagerItem {
6
- id: number;
7
- type: "page" | "link" | "system";
6
+ id: string;
7
+ type: "link" | "system";
8
8
  label: string;
9
9
  url: string;
10
- pageId: number | null;
11
10
  }
12
11
 
13
12
  export interface SystemNavConfig {
@@ -17,17 +16,10 @@ export interface SystemNavConfig {
17
16
  description: string;
18
17
  }
19
18
 
20
- export interface AvailablePage {
21
- id: number;
22
- title: string;
23
- slug: string;
24
- }
25
-
26
19
  export interface NavManagerLabels {
27
20
  preview: string;
28
21
  navigationItems: string;
29
22
  emptyState: string;
30
- page: string;
31
23
  link: string;
32
24
  system: string;
33
25
  toggleEdit: string;
@@ -35,7 +27,6 @@ export interface NavManagerLabels {
35
27
  url: string;
36
28
  save: string;
37
29
  delete: string;
38
- editPage: string;
39
30
  remove: string;
40
31
  orderSaved: string;
41
32
  labelRequired: string;
@@ -43,30 +34,27 @@ export interface NavManagerLabels {
43
34
  deleteFailed: string;
44
35
  systemLinks: string;
45
36
  systemLinksDescription: string;
46
- addPageToNavigation: string;
47
37
  addCustomLinkToNavigation: string;
48
- choosePage: string;
49
- searchPages: string;
50
- noPagesFound: string;
51
38
  addLink: string;
52
39
  addLinkDescription: string;
53
- allPagesInNav: string;
54
40
  urlPlaceholder: string;
55
41
  labelAndUrlRequired: string;
56
42
  maxVisibleLinks: string;
43
+ maxVisibleLinksDescription: string;
57
44
  maxVisibleSaved: string;
58
45
  useFeaturedAsDefault: string;
46
+ useFeaturedAsDefaultDescription: string;
59
47
  homeViewSaved: string;
60
48
  latest: string;
61
49
  featured: string;
62
50
  }
63
51
 
64
52
  export interface NavManagerUpdateDetail {
65
- id: number;
53
+ id: string;
66
54
  label: string;
67
55
  url?: string;
68
56
  }
69
57
 
70
58
  export interface NavManagerDeleteDetail {
71
- id: number;
59
+ id: string;
72
60
  }
@@ -1,6 +1,91 @@
1
1
  import { html, nothing } from "lit";
2
+ import { unsafeSVG } from "lit/directives/unsafe-svg.js";
2
3
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
3
4
  import type { JantPostForm } from "./jant-post-form.js";
5
+ import type { PostMediaItem } from "./post-form-types.js";
6
+ import { getMediaCategory } from "../../lib/upload.js";
7
+
8
+ function renderFileIcon(mimeType: string) {
9
+ const doc = `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>`;
10
+
11
+ let inner: string;
12
+ if (mimeType === "application/pdf") {
13
+ inner = `<text x="12" y="16.5" text-anchor="middle" fill="currentColor" stroke="none" font-size="6" font-weight="700" font-family="system-ui, sans-serif">PDF</text>`;
14
+ } else if (mimeType === "text/markdown") {
15
+ inner = `<text x="12" y="16.5" text-anchor="middle" fill="currentColor" stroke="none" font-size="10" font-weight="700" font-family="system-ui, sans-serif">#</text>`;
16
+ } else if (mimeType === "text/csv") {
17
+ inner = `<line x1="8" y1="12" x2="16" y2="12"/><line x1="8" y1="15" x2="16" y2="15"/><line x1="8" y1="18" x2="16" y2="18"/><line x1="10.7" y1="12" x2="10.7" y2="18"/><line x1="13.3" y1="12" x2="13.3" y2="18"/>`;
18
+ } else if (getMediaCategory(mimeType) === "archive") {
19
+ inner = `<line x1="12" y1="10" x2="12" y2="11.5"/><line x1="12" y1="13" x2="12" y2="14.5"/><line x1="12" y1="16" x2="12" y2="17.5"/>`;
20
+ } else if (mimeType.startsWith("audio/")) {
21
+ return html`<svg
22
+ width="24"
23
+ height="24"
24
+ viewBox="0 0 24 24"
25
+ fill="none"
26
+ stroke="currentColor"
27
+ stroke-width="1.5"
28
+ stroke-linecap="round"
29
+ stroke-linejoin="round"
30
+ >
31
+ ${unsafeSVG(
32
+ `<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>`,
33
+ )}
34
+ </svg>`;
35
+ } else if (mimeType.startsWith("video/")) {
36
+ return html`<svg
37
+ width="24"
38
+ height="24"
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ stroke-width="1.5"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ >
46
+ ${unsafeSVG(
47
+ `<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>`,
48
+ )}
49
+ </svg>`;
50
+ } else {
51
+ inner = `<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>`;
52
+ }
53
+
54
+ return html`<svg
55
+ width="24"
56
+ height="24"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="1.5"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ >
64
+ ${unsafeSVG(doc + inner)}
65
+ </svg>`;
66
+ }
67
+
68
+ function renderMediaThumb(item: PostMediaItem) {
69
+ const category = getMediaCategory(item.mimeType);
70
+
71
+ if (category === "image") {
72
+ return html`<img
73
+ src=${item.thumbUrl}
74
+ alt=${item.alt}
75
+ class="w-full h-full object-cover rounded-lg border"
76
+ loading="lazy"
77
+ />`;
78
+ }
79
+
80
+ return html`<div
81
+ class="w-full h-full rounded-lg border bg-muted flex flex-col items-center justify-center gap-1 p-1 text-muted-foreground"
82
+ >
83
+ ${renderFileIcon(item.mimeType)}
84
+ <span class="text-[10px] leading-tight text-center truncate w-full px-1"
85
+ >${item.originalName}</span
86
+ >
87
+ </div>`;
88
+ }
4
89
 
5
90
  function renderMediaList(component: JantPostForm) {
6
91
  const { media, labels, _mediaIds } = component;
@@ -32,12 +117,7 @@ function renderMediaList(component: JantPostForm) {
32
117
  }
33
118
 
34
119
  return html`<div class="relative group aspect-square" data-media-id=${id}>
35
- <img
36
- src=${item.thumbUrl}
37
- alt=${item.alt}
38
- class="w-full h-full object-cover rounded-lg border"
39
- loading="lazy"
40
- />
120
+ ${renderMediaThumb(item)}
41
121
  <button
42
122
  type="button"
43
123
  class="absolute top-1 right-1 w-5 h-5 flex items-center justify-center bg-black/60 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
@@ -107,18 +187,33 @@ export function renderPostForm(component: JantPostForm) {
107
187
  class="input"
108
188
  placeholder=${component.labels.titlePlaceholder}
109
189
  .value=${component._title}
110
- @input=${(e: Event) => component.handleInput("_title", e)}
190
+ @input=${(e: Event) => component.handleTitleInput(e)}
191
+ />
192
+ </div>
193
+
194
+ <div class="field">
195
+ <label class="label">${component.labels.slugLabel}</label>
196
+ <input
197
+ type="text"
198
+ class="input"
199
+ placeholder=${component.labels.slugPlaceholder}
200
+ .value=${component._slug}
201
+ @input=${(e: Event) => component.handleSlugInput(e)}
111
202
  />
203
+ ${component._slug
204
+ ? html`<p class="text-xs text-muted-foreground mt-1">
205
+ ${component.siteUrl}/${component._slug}
206
+ </p>`
207
+ : html`<p class="text-xs text-muted-foreground mt-1">
208
+ ${component.labels.slugHelp}
209
+ </p>`}
112
210
  </div>
113
211
 
114
212
  <div class="field">
115
213
  <label class="label">${component.labels.bodyLabel}</label>
116
- <textarea
117
- class="textarea min-h-32"
118
- placeholder=${component.labels.bodyPlaceholder}
119
- .value=${component._body}
120
- @input=${(e: Event) => component.handleInput("_body", e)}
121
- ></textarea>
214
+ <div
215
+ class="post-form-tiptap-body compose-tiptap-body border rounded-lg p-3 min-h-32"
216
+ ></div>
122
217
  </div>
123
218
 
124
219
  <div class="field">
@@ -173,33 +268,37 @@ export function renderPostForm(component: JantPostForm) {
173
268
  </select>
174
269
  </div>
175
270
 
176
- <div class="flex gap-4">
177
- <label class="flex items-center gap-2 text-sm">
178
- <input
179
- type="checkbox"
180
- class="checkbox"
181
- .checked=${component._featured}
182
- @change=${(e: Event) => {
183
- const target = e.target as HTMLInputElement;
184
- component._featured = target.checked;
185
- }}
186
- />
187
- ${component.labels.featuredLabel}
188
- </label>
189
- <label class="flex items-center gap-2 text-sm">
190
- <input
191
- type="checkbox"
192
- class="checkbox"
193
- .checked=${component._pinned}
194
- @change=${(e: Event) => {
195
- const target = e.target as HTMLInputElement;
196
- component._pinned = target.checked;
197
- }}
198
- />
199
- ${component.labels.pinnedLabel}
200
- </label>
271
+ <div class="field">
272
+ <label class="label">${component.labels.visibilityLabel}</label>
273
+ <select
274
+ class="select"
275
+ .value=${component._visibility}
276
+ @change=${(e: Event) => {
277
+ const target = e.target as HTMLSelectElement;
278
+ component._visibility =
279
+ (target.value as typeof component._visibility) ?? "public";
280
+ }}
281
+ >
282
+ <option value="public">${component.labels.visibilityPublic}</option>
283
+ <option value="unlisted">
284
+ ${component.labels.visibilityUnlisted}
285
+ </option>
286
+ </select>
201
287
  </div>
202
288
 
289
+ <label class="flex items-center gap-2 text-sm">
290
+ <input
291
+ type="checkbox"
292
+ class="checkbox"
293
+ .checked=${component._pinned}
294
+ @change=${(e: Event) => {
295
+ const target = e.target as HTMLInputElement;
296
+ component._pinned = target.checked;
297
+ }}
298
+ />
299
+ ${component.labels.pinnedLabel}
300
+ </label>
301
+
203
302
  ${renderCollections(component)}
204
303
 
205
304
  <div class="flex gap-2">
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Shared type definitions for the dashboard post form Lit component.
2
+ * Shared type definitions for the post form Lit component.
3
3
  */
4
4
 
5
5
  export type PostFormat = "note" | "link" | "quote";
6
6
  export type PostStatus = "published" | "draft";
7
+ export type PostVisibility = "public" | "unlisted";
7
8
 
8
9
  export interface PostFormLabels {
9
10
  formatLabel: string;
@@ -12,6 +13,9 @@ export interface PostFormLabels {
12
13
  quoteOption: string;
13
14
  titleLabel: string;
14
15
  titlePlaceholder: string;
16
+ slugLabel: string;
17
+ slugPlaceholder: string;
18
+ slugHelp: string;
15
19
  bodyLabel: string;
16
20
  bodyPlaceholder: string;
17
21
  urlLabel: string;
@@ -25,7 +29,9 @@ export interface PostFormLabels {
25
29
  statusLabel: string;
26
30
  statusPublished: string;
27
31
  statusDraft: string;
28
- featuredLabel: string;
32
+ visibilityLabel: string;
33
+ visibilityPublic: string;
34
+ visibilityUnlisted: string;
29
35
  pinnedLabel: string;
30
36
  collectionsLabel: string;
31
37
  submitLabel: string;
@@ -35,16 +41,18 @@ export interface PostFormLabels {
35
41
  mediaDialogLoading: string;
36
42
  submitSuccessMessage: string;
37
43
  submitErrorMessage: string;
44
+ draftFallbackMessage: string;
38
45
  }
39
46
 
40
47
  export interface PostFormInitial {
41
48
  format: PostFormat;
42
49
  title: string;
50
+ slug: string;
43
51
  body: string;
44
52
  url: string;
45
53
  quoteText: string;
46
54
  status: PostStatus;
47
- featured: boolean;
55
+ visibility: PostVisibility;
48
56
  pinned: boolean;
49
57
  rating: number;
50
58
  collectionIds: number[];
@@ -62,6 +70,8 @@ export interface PostMediaItem {
62
70
  id: string;
63
71
  thumbUrl: string;
64
72
  alt: string;
73
+ mimeType: string;
74
+ originalName: string;
65
75
  }
66
76
 
67
77
  export interface PostSubmitDetail {
@@ -70,9 +80,10 @@ export interface PostSubmitDetail {
70
80
  data: {
71
81
  format: PostFormat;
72
82
  title?: string;
83
+ slug?: string;
73
84
  body?: string;
74
85
  status: PostStatus;
75
- featured: boolean;
86
+ visibility: PostVisibility;
76
87
  pinned: boolean;
77
88
  url?: string;
78
89
  quoteText?: string;