@jant/core 0.3.36 → 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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -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/general",
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/general/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,15 +34,9 @@ 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
- createPage: string;
55
40
  urlPlaceholder: string;
56
41
  labelAndUrlRequired: string;
57
42
  maxVisibleLinks: string;
@@ -65,11 +50,11 @@ export interface NavManagerLabels {
65
50
  }
66
51
 
67
52
  export interface NavManagerUpdateDetail {
68
- id: number;
53
+ id: string;
69
54
  label: string;
70
55
  url?: string;
71
56
  }
72
57
 
73
58
  export interface NavManagerDeleteDetail {
74
- id: number;
59
+ id: string;
75
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,10 +187,28 @@ 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)}
111
191
  />
112
192
  </div>
113
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)}
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>`}
210
+ </div>
211
+
114
212
  <div class="field">
115
213
  <label class="label">${component.labels.bodyLabel}</label>
116
214
  <div
@@ -178,13 +276,10 @@ export function renderPostForm(component: JantPostForm) {
178
276
  @change=${(e: Event) => {
179
277
  const target = e.target as HTMLSelectElement;
180
278
  component._visibility =
181
- (target.value as typeof component._visibility) ?? "listed";
279
+ (target.value as typeof component._visibility) ?? "public";
182
280
  }}
183
281
  >
184
- <option value="listed">${component.labels.visibilityListed}</option>
185
- <option value="featured">
186
- ${component.labels.visibilityFeatured}
187
- </option>
282
+ <option value="public">${component.labels.visibilityPublic}</option>
188
283
  <option value="unlisted">
189
284
  ${component.labels.visibilityUnlisted}
190
285
  </option>
@@ -1,10 +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 = "listed" | "featured" | "unlisted";
7
+ export type PostVisibility = "public" | "unlisted";
8
8
 
9
9
  export interface PostFormLabels {
10
10
  formatLabel: string;
@@ -13,6 +13,9 @@ export interface PostFormLabels {
13
13
  quoteOption: string;
14
14
  titleLabel: string;
15
15
  titlePlaceholder: string;
16
+ slugLabel: string;
17
+ slugPlaceholder: string;
18
+ slugHelp: string;
16
19
  bodyLabel: string;
17
20
  bodyPlaceholder: string;
18
21
  urlLabel: string;
@@ -27,8 +30,7 @@ export interface PostFormLabels {
27
30
  statusPublished: string;
28
31
  statusDraft: string;
29
32
  visibilityLabel: string;
30
- visibilityListed: string;
31
- visibilityFeatured: string;
33
+ visibilityPublic: string;
32
34
  visibilityUnlisted: string;
33
35
  pinnedLabel: string;
34
36
  collectionsLabel: string;
@@ -39,11 +41,13 @@ export interface PostFormLabels {
39
41
  mediaDialogLoading: string;
40
42
  submitSuccessMessage: string;
41
43
  submitErrorMessage: string;
44
+ draftFallbackMessage: string;
42
45
  }
43
46
 
44
47
  export interface PostFormInitial {
45
48
  format: PostFormat;
46
49
  title: string;
50
+ slug: string;
47
51
  body: string;
48
52
  url: string;
49
53
  quoteText: string;
@@ -66,6 +70,8 @@ export interface PostMediaItem {
66
70
  id: string;
67
71
  thumbUrl: string;
68
72
  alt: string;
73
+ mimeType: string;
74
+ originalName: string;
69
75
  }
70
76
 
71
77
  export interface PostSubmitDetail {
@@ -74,6 +80,7 @@ export interface PostSubmitDetail {
74
80
  data: {
75
81
  format: PostFormat;
76
82
  title?: string;
83
+ slug?: string;
77
84
  body?: string;
78
85
  status: PostStatus;
79
86
  visibility: PostVisibility;