@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
@@ -12,7 +12,7 @@ import { LitElement, html, nothing } from "lit";
12
12
  import type { Editor, JSONContent } from "@tiptap/core";
13
13
  import type { ComposeLabels } from "./compose-types.js";
14
14
  import { createTiptapEditor } from "../tiptap/create-editor.js";
15
- import { getSlashCommands } from "../tiptap/slash-commands.js";
15
+ import { uploadWithMetadata } from "../upload-with-metadata.js";
16
16
 
17
17
  export class JantComposeFullscreen extends LitElement {
18
18
  static properties = {
@@ -20,14 +20,12 @@ export class JantComposeFullscreen extends LitElement {
20
20
  _open: { state: true },
21
21
  _title: { state: true },
22
22
  _showTitle: { state: true },
23
- _actionsOpen: { state: true },
24
23
  };
25
24
 
26
25
  declare labels: ComposeLabels;
27
26
  declare _open: boolean;
28
27
  declare _title: string;
29
28
  declare _showTitle: boolean;
30
- declare _actionsOpen: boolean;
31
29
 
32
30
  private _editor: Editor | null = null;
33
31
  private _content: JSONContent | null = null;
@@ -43,7 +41,6 @@ export class JantComposeFullscreen extends LitElement {
43
41
  this._open = false;
44
42
  this._title = "";
45
43
  this._showTitle = false;
46
- this._actionsOpen = false;
47
44
  }
48
45
 
49
46
  connectedCallback() {
@@ -96,14 +93,7 @@ export class JantComposeFullscreen extends LitElement {
96
93
  this._editor.chain().focus().setImage({ src: placeholderUrl }).run();
97
94
 
98
95
  try {
99
- const formData = new FormData();
100
- formData.append("file", file);
101
- const response = await fetch("/api/upload", {
102
- method: "POST",
103
- body: formData,
104
- });
105
- if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
106
- const data = (await response.json()) as { url: string };
96
+ const data = await uploadWithMetadata(file);
107
97
 
108
98
  // Replace placeholder with real URL
109
99
  const { doc } = this._editor.state;
@@ -161,7 +151,6 @@ export class JantComposeFullscreen extends LitElement {
161
151
  // Always show title in fullscreen — it's the primary editing surface
162
152
  this._showTitle = true;
163
153
  this._open = true;
164
- this._actionsOpen = false;
165
154
  // Show as modal (top layer) and init editor after render
166
155
  this.updateComplete.then(() => {
167
156
  const dialog = this.querySelector<HTMLDialogElement>(
@@ -221,51 +210,10 @@ export class JantComposeFullscreen extends LitElement {
221
210
  );
222
211
  }
223
212
 
224
- private _toggleActions() {
225
- this._actionsOpen = !this._actionsOpen;
226
- }
227
-
228
- private _executeCommand(index: number) {
229
- const commands = getSlashCommands();
230
- const item = commands[index];
231
- if (!item || !this._editor) {
232
- this._actionsOpen = false;
233
- return;
234
- }
235
-
236
- // Image command: trigger file picker directly
237
- if (item.label === "Image") {
238
- this._actionsOpen = false;
239
- this._triggerImagePicker();
240
- return;
241
- }
242
-
243
- const { from, to } = this._editor.state.selection;
244
- item.command(this._editor, { from, to });
245
- this._actionsOpen = false;
246
- }
247
-
248
- private _renderActionsMenu() {
249
- if (!this._actionsOpen) return nothing;
250
- const commands = getSlashCommands();
251
- return html`
252
- <div class="tiptap-slash-menu compose-fullscreen-plus-dropdown">
253
- ${commands.map(
254
- (item, i) => html`
255
- <div
256
- class="tiptap-slash-item"
257
- @mousedown=${(e: Event) => {
258
- e.preventDefault();
259
- this._executeCommand(i);
260
- }}
261
- >
262
- <span class="tiptap-slash-item-icon">${item.icon}</span>
263
- <span class="tiptap-slash-item-label">${item.label}</span>
264
- </div>
265
- `,
266
- )}
267
- </div>
268
- `;
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();
269
217
  }
270
218
 
271
219
  render() {
@@ -275,27 +223,24 @@ export class JantComposeFullscreen extends LitElement {
275
223
  <dialog class="compose-fullscreen-dialog" @cancel=${this._onDialogCancel}>
276
224
  <div class="compose-fullscreen">
277
225
  <div class="compose-fullscreen-toolbar">
278
- <div class="compose-fullscreen-plus-menu">
279
- <button
280
- type="button"
281
- class="compose-tool-btn"
282
- @click=${() => this._toggleActions()}
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"
283
239
  >
284
- <svg
285
- width="18"
286
- height="18"
287
- viewBox="0 0 18 18"
288
- fill="none"
289
- stroke="currentColor"
290
- stroke-width="2"
291
- stroke-linecap="round"
292
- >
293
- <line x1="9" y1="3" x2="9" y2="15" />
294
- <line x1="3" y1="9" x2="15" y2="9" />
295
- </svg>
296
- </button>
297
- ${this._renderActionsMenu()}
298
- </div>
240
+ <line x1="9" y1="3" x2="9" y2="15" />
241
+ <line x1="3" y1="9" x2="15" y2="9" />
242
+ </svg>
243
+ </button>
299
244
  <div class="flex-1"></div>
300
245
  <button
301
246
  type="button"
@@ -17,6 +17,7 @@ interface LightboxImage {
17
17
  width?: number;
18
18
  height?: number;
19
19
  mimeType?: string;
20
+ posterUrl?: string;
20
21
  }
21
22
 
22
23
  export class JantMediaLightbox extends LitElement {
@@ -197,6 +198,7 @@ export class JantMediaLightbox extends LitElement {
197
198
  ? html`<video
198
199
  class="media-lightbox-video"
199
200
  src=${img.url}
201
+ poster=${img.posterUrl ?? ""}
200
202
  controls
201
203
  autoplay
202
204
  playsinline
@@ -5,7 +5,7 @@
5
5
  * - Renders a preview bar that reflects current item order
6
6
  * - Sortable list with inline edit/delete panels
7
7
  * - SortableJS drag-and-drop reorder with immediate preview update
8
- * - Add page/link forms
8
+ * - Add link forms
9
9
  * - System nav item toggles with immediate list/preview update
10
10
  * - Dispatches events for update/delete (handled by bridge)
11
11
  *
@@ -17,7 +17,6 @@ import type { PropertyValueMap } from "lit";
17
17
  import Sortable from "sortablejs";
18
18
  import { showToast } from "../toast.js";
19
19
  import type {
20
- AvailablePage,
21
20
  NavManagerItem,
22
21
  NavManagerLabels,
23
22
  NavManagerUpdateDetail,
@@ -30,7 +29,6 @@ export class JantNavManager extends LitElement {
30
29
  items: { type: Array },
31
30
  labels: { type: Object },
32
31
  systemNavItems: { type: Array, attribute: "system-nav-items" },
33
- availablePages: { type: Array, attribute: "available-pages" },
34
32
  siteName: { type: String, attribute: "site-name" },
35
33
  maxVisible: { type: Number, attribute: "max-visible" },
36
34
  homeDefaultView: { type: String, attribute: "home-default-view" },
@@ -41,40 +39,30 @@ export class JantNavManager extends LitElement {
41
39
  _editUrl: { state: true },
42
40
  _togglingKeys: { state: true },
43
41
  _showOverflow: { state: true },
44
- _showPagePicker: { state: true },
45
42
  _showLinkForm: { state: true },
46
43
  _newLinkLabel: { state: true },
47
44
  _newLinkUrl: { state: true },
48
- _availablePages: { state: true },
49
- _addingPageId: { state: true },
50
45
  _addingLink: { state: true },
51
- _pageSearchQuery: { state: true },
52
46
  };
53
47
 
54
48
  declare items: NavManagerItem[];
55
49
  declare labels: NavManagerLabels;
56
50
  declare systemNavItems: SystemNavConfig[];
57
- declare availablePages: AvailablePage[];
58
51
  declare siteName: string;
59
52
  declare maxVisible: number;
60
53
  declare homeDefaultView: string;
61
54
 
62
55
  declare _items: NavManagerItem[];
63
- declare _editingId: number | null;
56
+ declare _editingId: string | null;
64
57
  declare _editLabel: string;
65
58
  declare _editUrl: string;
66
59
  /** Keys currently mid-request (to disable switch during toggle) */
67
60
  declare _togglingKeys: Set<string>;
68
61
  declare _showOverflow: boolean;
69
- declare _showPagePicker: boolean;
70
62
  declare _showLinkForm: boolean;
71
63
  declare _newLinkLabel: string;
72
64
  declare _newLinkUrl: string;
73
- declare _availablePages: AvailablePage[];
74
- /** Page ID currently being added (to disable its button) */
75
- declare _addingPageId: number | null;
76
65
  declare _addingLink: boolean;
77
- declare _pageSearchQuery: string;
78
66
 
79
67
  #sortable: { destroy(): void } | null = null;
80
68
  #initialized = false;
@@ -82,11 +70,6 @@ export class JantNavManager extends LitElement {
82
70
  this._showOverflow = false;
83
71
  document.removeEventListener("click", this.#closeOverflow);
84
72
  };
85
- #closePagePicker = () => {
86
- this._showPagePicker = false;
87
- this._pageSearchQuery = "";
88
- document.removeEventListener("click", this.#closePagePicker);
89
- };
90
73
  #closeLinkForm = () => {
91
74
  this._showLinkForm = false;
92
75
  document.removeEventListener("click", this.#closeLinkForm);
@@ -102,7 +85,6 @@ export class JantNavManager extends LitElement {
102
85
  this.items = [];
103
86
  this.labels = {} as NavManagerLabels;
104
87
  this.systemNavItems = [];
105
- this.availablePages = [];
106
88
  this.siteName = "";
107
89
  this.maxVisible = 2;
108
90
  this.homeDefaultView = "latest";
@@ -113,14 +95,10 @@ export class JantNavManager extends LitElement {
113
95
  this._editUrl = "";
114
96
  this._togglingKeys = new Set();
115
97
  this._showOverflow = false;
116
- this._showPagePicker = false;
117
98
  this._showLinkForm = false;
118
99
  this._newLinkLabel = "";
119
100
  this._newLinkUrl = "";
120
- this._availablePages = [];
121
- this._addingPageId = null;
122
101
  this._addingLink = false;
123
- this._pageSearchQuery = "";
124
102
  }
125
103
 
126
104
  protected update(changedProperties: PropertyValueMap<JantNavManager>): void {
@@ -128,9 +106,6 @@ export class JantNavManager extends LitElement {
128
106
  this._items = [...(this.items ?? [])];
129
107
  this.#initialized = true;
130
108
  }
131
- if (changedProperties.has("availablePages" as keyof JantNavManager)) {
132
- this._availablePages = [...(this.availablePages ?? [])];
133
- }
134
109
  super.update(changedProperties);
135
110
  }
136
111
 
@@ -143,7 +118,6 @@ export class JantNavManager extends LitElement {
143
118
  this.#sortable?.destroy();
144
119
  this.#sortable = null;
145
120
  document.removeEventListener("click", this.#closeOverflow);
146
- document.removeEventListener("click", this.#closePagePicker);
147
121
  document.removeEventListener("click", this.#closeLinkForm);
148
122
  }
149
123
 
@@ -161,7 +135,9 @@ export class JantNavManager extends LitElement {
161
135
  onEnd: (evt) => {
162
136
  // Read new order from DOM BEFORE reverting
163
137
  const els = [...list.querySelectorAll<HTMLElement>("[data-nav-id]")];
164
- const ids = els.map((el) => Number(el.dataset.navId));
138
+ const ids = els
139
+ .map((el) => el.dataset.navId)
140
+ .filter((id): id is string => id !== undefined);
165
141
 
166
142
  // Revert SortableJS DOM manipulation so Lit can re-render cleanly.
167
143
  // SortableJS physically moved the element — put it back where it was.
@@ -182,17 +158,28 @@ export class JantNavManager extends LitElement {
182
158
  this.#sortable?.destroy();
183
159
  this.#sortable = null;
184
160
 
161
+ // Find the moved item and compute neighbors
162
+ const movedId = newIndex != null ? ids[newIndex] : undefined;
163
+ if (!movedId) return;
164
+
165
+ const movedIdx = ids.indexOf(movedId);
166
+ const afterId = movedIdx > 0 ? ids[movedIdx - 1] : null;
167
+ const beforeId = movedIdx < ids.length - 1 ? ids[movedIdx + 1] : null;
168
+
185
169
  // Update internal state so Lit re-renders in the new order
186
170
  const itemMap = new Map(this._items.map((i) => [i.id, i]));
187
171
  this._items = ids
188
172
  .map((id) => itemMap.get(id))
189
173
  .filter((i): i is NavManagerItem => i !== undefined);
190
174
 
191
- // Persist to server
192
- fetch("/api/nav-items/reorder", {
175
+ // Persist to server — single item move
176
+ fetch(`/api/nav-items/${movedId}/move`, {
193
177
  method: "PUT",
194
178
  headers: { "Content-Type": "application/json" },
195
- body: JSON.stringify({ ids }),
179
+ body: JSON.stringify({
180
+ after: afterId ?? null,
181
+ before: beforeId ?? null,
182
+ }),
196
183
  }).then((res) => {
197
184
  if (res.ok) showToast(this.labels.orderSaved);
198
185
  else showToast(this.labels.saveFailed, "error");
@@ -253,7 +240,7 @@ export class JantNavManager extends LitElement {
253
240
  const clamped = Math.max(0, Math.min(5, value));
254
241
  this.maxVisible = clamped;
255
242
  try {
256
- const res = await fetch("/dash/settings/navigation/nav-max-visible", {
243
+ const res = await fetch("/settings/navigation/nav-max-visible", {
257
244
  method: "POST",
258
245
  headers: { "Content-Type": "application/json" },
259
246
  body: JSON.stringify({ value: clamped }),
@@ -268,7 +255,7 @@ export class JantNavManager extends LitElement {
268
255
  async #handleHomeViewToggle(useFeatured: boolean) {
269
256
  this.homeDefaultView = useFeatured ? "featured" : "latest";
270
257
  try {
271
- const res = await fetch("/dash/settings/navigation/home-default-view", {
258
+ const res = await fetch("/settings/navigation/home-default-view", {
272
259
  method: "POST",
273
260
  headers: { "Content-Type": "application/json" },
274
261
  body: JSON.stringify({ value: this.homeDefaultView }),
@@ -281,41 +268,9 @@ export class JantNavManager extends LitElement {
281
268
  }
282
269
 
283
270
  // ===========================================================================
284
- // Add page / link handlers
271
+ // Add link handler
285
272
  // ===========================================================================
286
273
 
287
- async #handleAddPage(page: AvailablePage) {
288
- this._addingPageId = page.id;
289
- try {
290
- const res = await fetch("/api/nav-items", {
291
- method: "POST",
292
- headers: {
293
- "Content-Type": "application/json",
294
- Accept: "application/json",
295
- },
296
- body: JSON.stringify({
297
- type: "page",
298
- label: page.title || page.slug,
299
- url: `/${page.slug}`,
300
- pageId: page.id,
301
- }),
302
- });
303
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
304
-
305
- const created: NavManagerItem = await res.json();
306
- this.#sortable?.destroy();
307
- this.#sortable = null;
308
- this._items = [...this._items, created];
309
- this._availablePages = this._availablePages.filter(
310
- (p) => p.id !== page.id,
311
- );
312
- } catch {
313
- showToast(this.labels.saveFailed, "error");
314
- } finally {
315
- this._addingPageId = null;
316
- }
317
- }
318
-
319
274
  async #handleAddLink() {
320
275
  const label = this._newLinkLabel.trim();
321
276
  const url = this._newLinkUrl.trim();
@@ -533,12 +488,7 @@ export class JantNavManager extends LitElement {
533
488
  }
534
489
 
535
490
  #renderTypeBadge(type: string) {
536
- const label =
537
- type === "page"
538
- ? this.labels.page
539
- : type === "system"
540
- ? this.labels.system
541
- : this.labels.link;
491
+ const label = type === "system" ? this.labels.system : this.labels.link;
542
492
  return html`<span class="badge-secondary">${label}</span>`;
543
493
  }
544
494
 
@@ -592,29 +542,6 @@ export class JantNavManager extends LitElement {
592
542
  `;
593
543
  }
594
544
 
595
- if (item.type === "page") {
596
- return html`
597
- <div class="nav-item-edit">
598
- <div class="flex items-center justify-between">
599
- <button
600
- type="button"
601
- class="btn-sm-ghost text-destructive"
602
- @click=${() => this.#handleDelete(item)}
603
- >
604
- ${this.labels.remove}
605
- </button>
606
- ${item.pageId
607
- ? html`<a
608
- href=${`/dash/pages/${item.pageId}/edit`}
609
- class="text-sm text-muted-foreground hover:text-foreground transition-colors"
610
- >${this.labels.editPage} &rarr;</a
611
- >`
612
- : nothing}
613
- </div>
614
- </div>
615
- `;
616
- }
617
-
618
545
  if (item.type === "system") {
619
546
  return html`
620
547
  <div class="nav-item-edit">
@@ -721,193 +648,6 @@ export class JantNavManager extends LitElement {
721
648
  `;
722
649
  }
723
650
 
724
- #renderAddArea() {
725
- return html`
726
- ${this.#renderAddPageSection()} ${this.#renderAddLinkSection()}
727
- `;
728
- }
729
-
730
- #renderAddPageSection() {
731
- const query = this._pageSearchQuery.toLowerCase();
732
- const filteredPages = query
733
- ? this._availablePages.filter((p) =>
734
- (p.title || p.slug).toLowerCase().includes(query),
735
- )
736
- : this._availablePages;
737
-
738
- return html`
739
- <section class="mt-8">
740
- <h2 class="text-lg font-semibold mb-3">
741
- ${this.labels.addPageToNavigation}
742
- </h2>
743
- <div id="nav-page-select" class="select">
744
- <button
745
- type="button"
746
- class="btn-outline w-full sm:w-[280px]"
747
- id="nav-page-select-trigger"
748
- aria-haspopup="listbox"
749
- aria-expanded=${this._showPagePicker}
750
- aria-controls="nav-page-select-listbox"
751
- @click=${(e: Event) => {
752
- e.stopPropagation();
753
- this._showPagePicker = !this._showPagePicker;
754
- this._pageSearchQuery = "";
755
- if (this._showPagePicker) {
756
- setTimeout(() => {
757
- document.addEventListener("click", this.#closePagePicker);
758
- this.querySelector<HTMLInputElement>(
759
- "#nav-page-search",
760
- )?.focus();
761
- });
762
- } else {
763
- document.removeEventListener("click", this.#closePagePicker);
764
- }
765
- }}
766
- >
767
- <span class="truncate">${this.labels.choosePage}</span>
768
- <svg
769
- xmlns="http://www.w3.org/2000/svg"
770
- width="24"
771
- height="24"
772
- viewBox="0 0 24 24"
773
- fill="none"
774
- stroke="currentColor"
775
- stroke-width="2"
776
- stroke-linecap="round"
777
- stroke-linejoin="round"
778
- class="text-muted-foreground opacity-50 shrink-0"
779
- >
780
- <path d="m7 15 5 5 5-5" />
781
- <path d="m7 9 5-5 5 5" />
782
- </svg>
783
- </button>
784
- ${this._showPagePicker
785
- ? html`
786
- <div
787
- id="nav-page-select-popover"
788
- data-popover
789
- aria-hidden="false"
790
- class="w-full sm:w-[280px]"
791
- @click=${(e: Event) => e.stopPropagation()}
792
- >
793
- <header>
794
- <svg
795
- xmlns="http://www.w3.org/2000/svg"
796
- width="24"
797
- height="24"
798
- viewBox="0 0 24 24"
799
- fill="none"
800
- stroke="currentColor"
801
- stroke-width="2"
802
- stroke-linecap="round"
803
- stroke-linejoin="round"
804
- >
805
- <circle cx="11" cy="11" r="8" />
806
- <path d="m21 21-4.3-4.3" />
807
- </svg>
808
- <input
809
- type="text"
810
- id="nav-page-search"
811
- .value=${this._pageSearchQuery}
812
- placeholder=${this.labels.searchPages}
813
- autocomplete="off"
814
- autocorrect="off"
815
- spellcheck="false"
816
- aria-autocomplete="list"
817
- role="combobox"
818
- aria-expanded="true"
819
- aria-controls="nav-page-select-listbox"
820
- aria-labelledby="nav-page-select-trigger"
821
- @input=${(e: Event) => {
822
- this._pageSearchQuery = (
823
- e.target as HTMLInputElement
824
- ).value;
825
- }}
826
- />
827
- </header>
828
- <div
829
- role="listbox"
830
- id="nav-page-select-listbox"
831
- aria-orientation="vertical"
832
- aria-labelledby="nav-page-select-trigger"
833
- data-empty=${this.labels.noPagesFound}
834
- >
835
- ${filteredPages.length > 0
836
- ? html`<div class="max-h-64 overflow-y-auto scrollbar">
837
- ${filteredPages.map(
838
- (page) => html`
839
- <div
840
- role="option"
841
- data-value=${page.id}
842
- @click=${() => {
843
- this._showPagePicker = false;
844
- this._pageSearchQuery = "";
845
- document.removeEventListener(
846
- "click",
847
- this.#closePagePicker,
848
- );
849
- this.#handleAddPage(page);
850
- }}
851
- >
852
- ${page.title || page.slug}
853
- </div>
854
- `,
855
- )}
856
- <hr role="separator" />
857
- <a href="/dash/pages/new" role="option">
858
- <svg
859
- xmlns="http://www.w3.org/2000/svg"
860
- width="24"
861
- height="24"
862
- viewBox="0 0 24 24"
863
- fill="none"
864
- stroke="currentColor"
865
- stroke-width="2"
866
- stroke-linecap="round"
867
- stroke-linejoin="round"
868
- >
869
- <circle cx="12" cy="12" r="10" />
870
- <path d="M8 12h8" />
871
- <path d="M12 8v8" />
872
- </svg>
873
- ${this.labels.createPage}
874
- </a>
875
- </div>`
876
- : html`<div
877
- class="py-6 text-center text-sm text-muted-foreground"
878
- >
879
- ${this._availablePages.length === 0
880
- ? this.labels.allPagesInNav
881
- : this.labels.noPagesFound}
882
- </div>
883
- <hr role="separator" />
884
- <a href="/dash/pages/new" role="option">
885
- <svg
886
- xmlns="http://www.w3.org/2000/svg"
887
- width="24"
888
- height="24"
889
- viewBox="0 0 24 24"
890
- fill="none"
891
- stroke="currentColor"
892
- stroke-width="2"
893
- stroke-linecap="round"
894
- stroke-linejoin="round"
895
- >
896
- <circle cx="12" cy="12" r="10" />
897
- <path d="M8 12h8" />
898
- <path d="M12 8v8" />
899
- </svg>
900
- ${this.labels.createPage}
901
- </a>`}
902
- </div>
903
- </div>
904
- `
905
- : nothing}
906
- </div>
907
- </section>
908
- `;
909
- }
910
-
911
651
  #renderAddLinkSection() {
912
652
  return html`
913
653
  <section class="mt-8">
@@ -1118,7 +858,7 @@ export class JantNavManager extends LitElement {
1118
858
  `}
1119
859
  </section>
1120
860
 
1121
- ${this.#renderAddArea()} ${this.#renderSystemToggles()}
861
+ ${this.#renderAddLinkSection()} ${this.#renderSystemToggles()}
1122
862
  `;
1123
863
  }
1124
864
  }