@jant/core 0.3.35 → 0.3.36

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 (156) hide show
  1. package/dist/client/assets/module-RjUF93sV.js +716 -0
  2. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  3. package/dist/client/assets/url-8Dj-5CLW.js +1 -0
  4. package/dist/client/client.css +1 -1
  5. package/dist/client/client.js +3109 -2294
  6. package/dist/index.js +3026 -2778
  7. package/package.json +13 -4
  8. package/src/__tests__/helpers/app.ts +1 -1
  9. package/src/__tests__/helpers/db.ts +6 -0
  10. package/src/app.tsx +1 -5
  11. package/src/{lib → client}/avatar-upload.ts +1 -1
  12. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  13. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  14. package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
  15. package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
  16. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
  17. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  18. package/src/client/components/collection-sidebar-types.ts +45 -0
  19. package/src/{ui → client}/components/collection-types.ts +3 -4
  20. package/src/{ui → client}/components/compose-types.ts +3 -1
  21. package/src/{ui → client}/components/jant-collection-form.ts +301 -182
  22. package/src/client/components/jant-collection-sidebar.ts +801 -0
  23. package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
  24. package/src/client/components/jant-compose-editor.ts +1249 -0
  25. package/src/client/components/jant-compose-fullscreen.ts +338 -0
  26. package/src/client/components/jant-media-lightbox.ts +257 -0
  27. package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
  28. package/src/{ui → client}/components/jant-post-form.ts +57 -8
  29. package/src/{ui → client}/components/jant-settings-general.ts +2 -2
  30. package/src/{ui → client}/components/nav-manager-types.ts +3 -0
  31. package/src/{ui → client}/components/post-form-template.ts +35 -31
  32. package/src/{ui → client}/components/post-form-types.ts +7 -3
  33. package/src/{lib → client}/compose-bridge.ts +9 -7
  34. package/src/client/lazy-slugify.ts +51 -0
  35. package/src/{lib → client}/media-upload.ts +16 -3
  36. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  37. package/src/client/page-slug-bridge.ts +42 -0
  38. package/src/{lib → client}/post-form-bridge.ts +2 -2
  39. package/src/{lib → client}/settings-bridge.ts +3 -3
  40. package/src/client/tiptap/bubble-menu.ts +205 -0
  41. package/src/client/tiptap/create-editor.ts +40 -0
  42. package/src/client/tiptap/exitable-marks.ts +73 -0
  43. package/src/client/tiptap/extensions.ts +60 -0
  44. package/src/client/tiptap/image-node.ts +488 -0
  45. package/src/client/tiptap/link-toolbar.ts +371 -0
  46. package/src/client/tiptap/more-break.ts +50 -0
  47. package/src/client/tiptap/paste-image.ts +140 -0
  48. package/src/client/tiptap/slash-commands.ts +328 -0
  49. package/src/{types → client/types}/sortablejs.d.ts +1 -1
  50. package/src/client.ts +24 -17
  51. package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
  52. package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
  53. package/src/db/schema.ts +6 -1
  54. package/src/i18n/locales/en.po +641 -215
  55. package/src/i18n/locales/en.ts +1 -1
  56. package/src/i18n/locales/zh-Hans.po +642 -204
  57. package/src/i18n/locales/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/zh-Hant.po +642 -204
  59. package/src/i18n/locales/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  61. package/src/lib/__tests__/schemas.test.ts +9 -6
  62. package/src/lib/__tests__/url.test.ts +2 -2
  63. package/src/lib/__tests__/view.test.ts +9 -9
  64. package/src/lib/emoji-catalog.ts +146 -0
  65. package/src/lib/feed.ts +1 -1
  66. package/src/lib/media-helpers.ts +10 -9
  67. package/src/lib/render.tsx +4 -3
  68. package/src/lib/resolve-config.ts +8 -1
  69. package/src/lib/schemas.ts +2 -3
  70. package/src/lib/summary.ts +92 -0
  71. package/src/lib/timeline.ts +2 -0
  72. package/src/lib/tiptap-render.ts +196 -0
  73. package/src/lib/upload.ts +97 -9
  74. package/src/lib/url.ts +7 -23
  75. package/src/lib/view.ts +33 -19
  76. package/src/middleware/error-handler.ts +3 -3
  77. package/src/preset.css +38 -0
  78. package/src/routes/api/collections.ts +20 -3
  79. package/src/routes/api/posts.ts +48 -33
  80. package/src/routes/api/upload.ts +7 -5
  81. package/src/routes/auth/reset.tsx +5 -4
  82. package/src/routes/auth/setup.tsx +26 -11
  83. package/src/routes/auth/signin.tsx +10 -7
  84. package/src/routes/compose.tsx +20 -11
  85. package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
  86. package/src/routes/dash/index.tsx +7 -1
  87. package/src/routes/dash/media.tsx +3 -0
  88. package/src/routes/dash/pages.tsx +8 -2
  89. package/src/routes/dash/posts.tsx +6 -2
  90. package/src/routes/dash/redirects.tsx +15 -9
  91. package/src/routes/dash/settings.tsx +336 -32
  92. package/src/routes/feed/__tests__/rss.test.ts +7 -7
  93. package/src/routes/feed/rss.ts +8 -6
  94. package/src/routes/pages/__tests__/featured.test.ts +6 -7
  95. package/src/routes/pages/archive.tsx +11 -7
  96. package/src/routes/pages/collection.tsx +32 -15
  97. package/src/routes/pages/collections.tsx +11 -2
  98. package/src/routes/pages/featured.tsx +1 -1
  99. package/src/routes/pages/home.tsx +1 -1
  100. package/src/services/__tests__/post.test.ts +124 -33
  101. package/src/services/__tests__/settings.test.ts +3 -3
  102. package/src/services/page.ts +16 -3
  103. package/src/services/post.ts +96 -37
  104. package/src/services/search.ts +4 -2
  105. package/src/services/settings.ts +6 -2
  106. package/src/styles/components.css +240 -60
  107. package/src/styles/tokens.css +10 -0
  108. package/src/styles/ui.css +1157 -81
  109. package/src/types/bindings.ts +5 -0
  110. package/src/types/config.ts +23 -1
  111. package/src/types/constants.ts +3 -0
  112. package/src/types/entities.ts +9 -2
  113. package/src/types/operations.ts +9 -3
  114. package/src/types/props.ts +3 -3
  115. package/src/types/views.ts +3 -2
  116. package/src/ui/compose/ComposeDialog.tsx +24 -7
  117. package/src/ui/dash/PageForm.tsx +2 -0
  118. package/src/ui/dash/PostList.tsx +5 -5
  119. package/src/ui/dash/StatusBadge.tsx +13 -5
  120. package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
  121. package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
  122. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  123. package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
  124. package/src/ui/dash/media/MediaListContent.tsx +9 -4
  125. package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
  126. package/src/ui/dash/pages/PagesContent.tsx +2 -1
  127. package/src/ui/dash/posts/PostForm.tsx +19 -7
  128. package/src/ui/dash/settings/AccountContent.tsx +133 -138
  129. package/src/ui/dash/settings/AvatarContent.tsx +70 -0
  130. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  131. package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
  132. package/src/ui/layouts/DashLayout.tsx +157 -75
  133. package/src/ui/layouts/SiteLayout.tsx +13 -13
  134. package/src/ui/pages/ArchivePage.tsx +10 -7
  135. package/src/ui/pages/CollectionPage.tsx +6 -35
  136. package/src/ui/pages/CollectionsPage.tsx +2 -1
  137. package/src/ui/pages/FeaturedPage.tsx +2 -1
  138. package/src/ui/pages/HomePage.tsx +1 -1
  139. package/src/ui/pages/SearchPage.tsx +1 -1
  140. package/src/ui/shared/CollectionsSidebar.tsx +228 -3
  141. package/src/ui/shared/MediaGallery.tsx +179 -41
  142. package/src/lib/collections-reorder.ts +0 -28
  143. package/src/routes/dash/appearance.tsx +0 -240
  144. package/src/routes/dash/collections.tsx +0 -211
  145. package/src/ui/components/jant-compose-editor.ts +0 -814
  146. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  147. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  148. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  149. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  150. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  151. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  152. /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
  153. /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
  154. /package/src/{ui → client}/components/settings-types.ts +0 -0
  155. /package/src/{lib → client}/image-processor.ts +0 -0
  156. /package/src/{lib → client}/toast.ts +0 -0
@@ -15,7 +15,7 @@
15
15
  import { LitElement, html, nothing } from "lit";
16
16
  import type { PropertyValueMap } from "lit";
17
17
  import Sortable from "sortablejs";
18
- import { showToast } from "../../lib/toast.js";
18
+ import { showToast } from "../toast.js";
19
19
  import type {
20
20
  AvailablePage,
21
21
  NavManagerItem,
@@ -104,7 +104,7 @@ export class JantNavManager extends LitElement {
104
104
  this.systemNavItems = [];
105
105
  this.availablePages = [];
106
106
  this.siteName = "";
107
- this.maxVisible = 3;
107
+ this.maxVisible = 2;
108
108
  this.homeDefaultView = "latest";
109
109
 
110
110
  this._items = [];
@@ -253,7 +253,7 @@ export class JantNavManager extends LitElement {
253
253
  const clamped = Math.max(0, Math.min(5, value));
254
254
  this.maxVisible = clamped;
255
255
  try {
256
- const res = await fetch("/dash/appearance/nav-max-visible", {
256
+ const res = await fetch("/dash/settings/navigation/nav-max-visible", {
257
257
  method: "POST",
258
258
  headers: { "Content-Type": "application/json" },
259
259
  body: JSON.stringify({ value: clamped }),
@@ -268,7 +268,7 @@ export class JantNavManager extends LitElement {
268
268
  async #handleHomeViewToggle(useFeatured: boolean) {
269
269
  this.homeDefaultView = useFeatured ? "featured" : "latest";
270
270
  try {
271
- const res = await fetch("/dash/appearance/home-default-view", {
271
+ const res = await fetch("/dash/settings/navigation/home-default-view", {
272
272
  method: "POST",
273
273
  headers: { "Content-Type": "application/json" },
274
274
  body: JSON.stringify({ value: this.homeDefaultView }),
@@ -420,11 +420,14 @@ export class JantNavManager extends LitElement {
420
420
  const hasMore = overflow.length > 0;
421
421
 
422
422
  return html`
423
- <div class="border rounded-lg">
424
- <p class="text-xs text-muted-foreground px-4 pt-3">
425
- ${this.labels.preview}
426
- </p>
427
- <div class="px-5 py-3">
423
+ <div class="nav-preview">
424
+ <div class="nav-preview-chrome">
425
+ <div class="nav-preview-dots">
426
+ <span></span><span></span><span></span>
427
+ </div>
428
+ <span class="nav-preview-label">${this.labels.preview}</span>
429
+ </div>
430
+ <div class="nav-preview-content">
428
431
  <div class="site-header-top">
429
432
  <a href="/" class="site-logo">${this.siteName}</a>
430
433
  <div class="site-header-right">
@@ -544,7 +547,7 @@ export class JantNavManager extends LitElement {
544
547
 
545
548
  if (item.type === "link") {
546
549
  return html`
547
- <div class="pb-4 pl-8 flex flex-col gap-3">
550
+ <div class="nav-item-edit">
548
551
  <div class="field">
549
552
  <label class="label">${this.labels.label}</label>
550
553
  <input
@@ -569,20 +572,20 @@ export class JantNavManager extends LitElement {
569
572
  }}
570
573
  />
571
574
  </div>
572
- <div class="flex gap-2">
575
+ <div class="flex items-center justify-between">
573
576
  <button
574
577
  type="button"
575
- class="btn-sm"
576
- @click=${() => this.#handleUpdate(item)}
578
+ class="btn-sm-ghost text-destructive"
579
+ @click=${() => this.#handleDelete(item)}
577
580
  >
578
- ${this.labels.save}
581
+ ${this.labels.delete}
579
582
  </button>
580
583
  <button
581
584
  type="button"
582
- class="btn-sm-ghost text-destructive"
583
- @click=${() => this.#handleDelete(item)}
585
+ class="btn-sm"
586
+ @click=${() => this.#handleUpdate(item)}
584
587
  >
585
- ${this.labels.delete}
588
+ ${this.labels.save}
586
589
  </button>
587
590
  </div>
588
591
  </div>
@@ -591,16 +594,8 @@ export class JantNavManager extends LitElement {
591
594
 
592
595
  if (item.type === "page") {
593
596
  return html`
594
- <div class="pb-4 pl-8 flex flex-col gap-3">
595
- <code class="text-sm text-muted-foreground">${item.url}</code>
596
- <div class="flex gap-2">
597
- ${item.pageId
598
- ? html`<a
599
- href=${`/dash/pages/${item.pageId}/edit`}
600
- class="btn-sm-outline"
601
- >${this.labels.editPage}</a
602
- >`
603
- : nothing}
597
+ <div class="nav-item-edit">
598
+ <div class="flex items-center justify-between">
604
599
  <button
605
600
  type="button"
606
601
  class="btn-sm-ghost text-destructive"
@@ -608,6 +603,13 @@ export class JantNavManager extends LitElement {
608
603
  >
609
604
  ${this.labels.remove}
610
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}
611
613
  </div>
612
614
  </div>
613
615
  `;
@@ -615,8 +617,7 @@ export class JantNavManager extends LitElement {
615
617
 
616
618
  if (item.type === "system") {
617
619
  return html`
618
- <div class="pb-4 pl-8 flex flex-col gap-3">
619
- <code class="text-sm text-muted-foreground">${item.url}</code>
620
+ <div class="nav-item-edit">
620
621
  <div class="field">
621
622
  <label class="label">${this.labels.label}</label>
622
623
  <input
@@ -629,20 +630,20 @@ export class JantNavManager extends LitElement {
629
630
  }}
630
631
  />
631
632
  </div>
632
- <div class="flex gap-2">
633
+ <div class="flex items-center justify-between">
633
634
  <button
634
635
  type="button"
635
- class="btn-sm"
636
- @click=${() => this.#handleUpdate(item)}
636
+ class="btn-sm-ghost text-destructive"
637
+ @click=${() => this.#handleDelete(item)}
637
638
  >
638
- ${this.labels.save}
639
+ ${this.labels.remove}
639
640
  </button>
640
641
  <button
641
642
  type="button"
642
- class="btn-sm-ghost text-destructive"
643
- @click=${() => this.#handleDelete(item)}
643
+ class="btn-sm"
644
+ @click=${() => this.#handleUpdate(item)}
644
645
  >
645
- ${this.labels.remove}
646
+ ${this.labels.save}
646
647
  </button>
647
648
  </div>
648
649
  </div>
@@ -653,17 +654,19 @@ export class JantNavManager extends LitElement {
653
654
  }
654
655
 
655
656
  #renderItem(item: NavManagerItem) {
657
+ const isEditing = this._editingId === item.id;
658
+
656
659
  return html`
657
- <div data-nav-id=${item.id}>
658
- <div class="flex items-center py-3 gap-2">
659
- <div
660
- class="flex items-center gap-3 cursor-grab flex-1 min-w-0"
661
- data-drag-handle
662
- >
660
+ <div
661
+ data-nav-id=${item.id}
662
+ class="nav-item${isEditing ? " nav-item-editing" : ""}"
663
+ >
664
+ <div class="nav-item-row">
665
+ <div class="nav-item-handle" data-drag-handle>
663
666
  <svg
664
667
  xmlns="http://www.w3.org/2000/svg"
665
- width="16"
666
- height="16"
668
+ width="14"
669
+ height="14"
667
670
  viewBox="0 0 24 24"
668
671
  fill="none"
669
672
  stroke="currentColor"
@@ -679,31 +682,39 @@ export class JantNavManager extends LitElement {
679
682
  <circle cx="15" cy="5" r="1" />
680
683
  <circle cx="15" cy="19" r="1" />
681
684
  </svg>
682
- <span class="font-medium truncate">${item.label}</span>
683
685
  </div>
684
- <div class="flex items-center gap-2 shrink-0">
685
- ${this.#renderTypeBadge(item.type)}
686
- <button
687
- type="button"
688
- class="btn-sm-ghost"
689
- @click=${() => this.#toggleEdit(item)}
690
- aria-label=${this.labels.toggleEdit}
686
+ <div class="nav-item-info" @click=${() => this.#toggleEdit(item)}>
687
+ <div class="flex items-center gap-2 min-w-0">
688
+ <span class="text-sm font-medium truncate">${item.label}</span>
689
+ ${this.#renderTypeBadge(item.type)}
690
+ </div>
691
+ <span class="text-xs text-muted-foreground truncate"
692
+ >${item.url}</span
691
693
  >
692
- <svg
693
- xmlns="http://www.w3.org/2000/svg"
694
- width="16"
695
- height="16"
696
- viewBox="0 0 24 24"
697
- fill="none"
698
- stroke="currentColor"
699
- stroke-width="2"
700
- stroke-linecap="round"
701
- stroke-linejoin="round"
702
- >
703
- <path d="m6 9 6 6 6-6" />
704
- </svg>
705
- </button>
706
694
  </div>
695
+ <button
696
+ type="button"
697
+ class="nav-item-toggle"
698
+ @click=${() => this.#toggleEdit(item)}
699
+ aria-label=${this.labels.toggleEdit}
700
+ >
701
+ <svg
702
+ xmlns="http://www.w3.org/2000/svg"
703
+ width="14"
704
+ height="14"
705
+ viewBox="0 0 24 24"
706
+ fill="none"
707
+ stroke="currentColor"
708
+ stroke-width="2"
709
+ stroke-linecap="round"
710
+ stroke-linejoin="round"
711
+ style="transition: transform 0.15s; ${isEditing
712
+ ? "transform: rotate(180deg);"
713
+ : ""}"
714
+ >
715
+ <path d="m6 9 6 6 6-6" />
716
+ </svg>
717
+ </button>
707
718
  </div>
708
719
  ${this.#renderEditPanel(item)}
709
720
  </div>
@@ -842,14 +853,52 @@ export class JantNavManager extends LitElement {
842
853
  </div>
843
854
  `,
844
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>
845
875
  </div>`
846
876
  : html`<div
847
- class="py-6 text-center text-sm text-muted-foreground"
848
- >
849
- ${this._availablePages.length === 0
850
- ? this.labels.allPagesInNav
851
- : this.labels.noPagesFound}
852
- </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>`}
853
902
  </div>
854
903
  </div>
855
904
  `
@@ -1007,15 +1056,20 @@ export class JantNavManager extends LitElement {
1007
1056
  return html`
1008
1057
  ${this.#renderPreview()}
1009
1058
 
1010
- <div class="flex flex-col gap-3 mt-3">
1011
- <div class="flex items-center gap-3">
1012
- <label class="text-sm" for="nav-max-visible">
1013
- ${this.labels.maxVisibleLinks}
1014
- </label>
1059
+ <div class="flex flex-col gap-4 mt-3">
1060
+ <div class="flex items-start justify-between gap-4">
1061
+ <div class="flex flex-col gap-0.5">
1062
+ <label class="text-sm font-medium" for="nav-max-visible">
1063
+ ${this.labels.maxVisibleLinks}
1064
+ </label>
1065
+ <p class="text-xs text-muted-foreground">
1066
+ ${this.labels.maxVisibleLinksDescription}
1067
+ </p>
1068
+ </div>
1015
1069
  <input
1016
1070
  type="number"
1017
1071
  id="nav-max-visible"
1018
- class="input w-16 h-8"
1072
+ class="input w-16 h-8 shrink-0"
1019
1073
  min="0"
1020
1074
  max="5"
1021
1075
  .value=${String(this.maxVisible)}
@@ -1025,15 +1079,20 @@ export class JantNavManager extends LitElement {
1025
1079
  }}
1026
1080
  />
1027
1081
  </div>
1028
- <div class="flex items-center gap-3">
1029
- <label class="text-sm" for="nav-home-view">
1030
- ${this.labels.useFeaturedAsDefault}
1031
- </label>
1082
+ <div class="flex items-start justify-between gap-4">
1083
+ <div class="flex flex-col gap-0.5">
1084
+ <label class="text-sm font-medium" for="nav-home-view">
1085
+ ${this.labels.useFeaturedAsDefault}
1086
+ </label>
1087
+ <p class="text-xs text-muted-foreground">
1088
+ ${this.labels.useFeaturedAsDefaultDescription}
1089
+ </p>
1090
+ </div>
1032
1091
  <input
1033
1092
  type="checkbox"
1034
1093
  role="switch"
1035
1094
  id="nav-home-view"
1036
- class="input"
1095
+ class="input shrink-0"
1037
1096
  .checked=${this.homeDefaultView === "featured"}
1038
1097
  @change=${(e: Event) => {
1039
1098
  this.#handleHomeViewToggle(
@@ -1053,7 +1112,7 @@ export class JantNavManager extends LitElement {
1053
1112
  ${this.labels.emptyState}
1054
1113
  </p>`
1055
1114
  : html`
1056
- <div id="nav-items-list" class="flex flex-col divide-y">
1115
+ <div id="nav-items-list" class="nav-items-list">
1057
1116
  ${this._items.map((item) => this.#renderItem(item))}
1058
1117
  </div>
1059
1118
  `}
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { LitElement } from "lit";
9
+ import type { Editor, JSONContent } from "@tiptap/core";
9
10
  import type {
10
11
  PostFormInitial,
11
12
  PostFormLabels,
@@ -14,8 +15,10 @@ import type {
14
15
  PostSubmitDetail,
15
16
  PostFormat,
16
17
  PostStatus,
18
+ PostVisibility,
17
19
  } from "./post-form-types.js";
18
20
  import { renderPostForm } from "./post-form-template.js";
21
+ import { createTiptapEditor } from "../tiptap/create-editor.js";
19
22
 
20
23
  const DEFAULT_INITIAL: PostFormInitial = {
21
24
  format: "note",
@@ -24,7 +27,7 @@ const DEFAULT_INITIAL: PostFormInitial = {
24
27
  url: "",
25
28
  quoteText: "",
26
29
  status: "published",
27
- featured: false,
30
+ visibility: "listed",
28
31
  pinned: false,
29
32
  rating: 0,
30
33
  collectionIds: [],
@@ -51,7 +54,10 @@ const EMPTY_LABELS: PostFormLabels = {
51
54
  statusLabel: "",
52
55
  statusPublished: "",
53
56
  statusDraft: "",
54
- featuredLabel: "",
57
+ visibilityLabel: "",
58
+ visibilityListed: "",
59
+ visibilityFeatured: "",
60
+ visibilityUnlisted: "",
55
61
  pinnedLabel: "",
56
62
  collectionsLabel: "",
57
63
  submitLabel: "",
@@ -93,7 +99,7 @@ export class JantPostForm extends LitElement {
93
99
  _url: { state: true },
94
100
  _quoteText: { state: true },
95
101
  _status: { state: true },
96
- _featured: { state: true },
102
+ _visibility: { state: true },
97
103
  _pinned: { state: true },
98
104
  _rating: { state: true },
99
105
  _collectionIds: { state: true },
@@ -115,13 +121,15 @@ export class JantPostForm extends LitElement {
115
121
  declare _url: string;
116
122
  declare _quoteText: string;
117
123
  declare _status: PostStatus;
118
- declare _featured: boolean;
124
+ declare _visibility: PostVisibility;
119
125
  declare _pinned: boolean;
120
126
  declare _rating: number;
121
127
  declare _collectionIds: number[];
122
128
  declare _mediaIds: string[];
123
129
  declare _loading: boolean;
124
130
 
131
+ _editor: Editor | null = null;
132
+ _bodyJson: JSONContent | null = null;
125
133
  #initialized = false;
126
134
 
127
135
  createRenderRoot() {
@@ -145,7 +153,7 @@ export class JantPostForm extends LitElement {
145
153
  this._url = "";
146
154
  this._quoteText = "";
147
155
  this._status = "published";
148
- this._featured = false;
156
+ this._visibility = "listed";
149
157
  this._pinned = false;
150
158
  this._rating = 0;
151
159
  this._collectionIds = [];
@@ -197,6 +205,8 @@ export class JantPostForm extends LitElement {
197
205
 
198
206
  disconnectedCallback() {
199
207
  super.disconnectedCallback();
208
+ this._editor?.destroy();
209
+ this._editor = null;
200
210
  this.closeMediaPicker();
201
211
  }
202
212
 
@@ -208,12 +218,46 @@ export class JantPostForm extends LitElement {
208
218
  this._url = init.url ?? "";
209
219
  this._quoteText = init.quoteText ?? "";
210
220
  this._status = init.status ?? "published";
211
- this._featured = !!init.featured;
221
+ this._visibility = init.visibility ?? "listed";
212
222
  this._pinned = !!init.pinned;
213
223
  this._rating = init.rating ?? 0;
214
224
  this._collectionIds = [...(init.collectionIds ?? [])];
215
225
  this._mediaIds = [...(init.mediaIds ?? [])];
216
226
  this.#initialized = true;
227
+
228
+ // Parse body as Tiptap JSON if it looks like JSON
229
+ if (this._body && this._body.startsWith("{")) {
230
+ try {
231
+ this._bodyJson = JSON.parse(this._body) as JSONContent;
232
+ } catch {
233
+ this._bodyJson = null;
234
+ }
235
+ } else {
236
+ this._bodyJson = null;
237
+ }
238
+ }
239
+
240
+ initEditor() {
241
+ if (this._editor) return;
242
+ const container = this.querySelector<HTMLElement>(".post-form-tiptap-body");
243
+ if (!container) return;
244
+
245
+ this._editor = createTiptapEditor({
246
+ element: container,
247
+ placeholder: this.labels.bodyPlaceholder ?? "Write something…",
248
+ content: this._bodyJson,
249
+ onUpdate: (json) => {
250
+ this._bodyJson = json;
251
+ this._body = JSON.stringify(json);
252
+ },
253
+ });
254
+ }
255
+
256
+ protected updated(changed: Map<string, unknown>) {
257
+ super.updated(changed);
258
+ if (!this._editor) {
259
+ this.initEditor();
260
+ }
217
261
  }
218
262
 
219
263
  handleInput(field: "_title" | "_body" | "_url" | "_quoteText", e: Event) {
@@ -253,15 +297,20 @@ export class JantPostForm extends LitElement {
253
297
  handleSubmit(e: Event) {
254
298
  e.preventDefault();
255
299
  if (this._loading || !this.action) return;
300
+ // Use Tiptap JSON for body
301
+ const bodyValue = this._bodyJson
302
+ ? JSON.stringify(this._bodyJson)
303
+ : this._body;
304
+
256
305
  const detail: PostSubmitDetail = {
257
306
  endpoint: this.action,
258
307
  isEdit: this.isEdit,
259
308
  data: {
260
309
  format: this._format,
261
310
  title: this._title.trim(),
262
- body: this._body,
311
+ body: bodyValue,
263
312
  status: this._status,
264
- featured: this._featured,
313
+ visibility: this._visibility,
265
314
  pinned: this._pinned,
266
315
  url: this._url.trim(),
267
316
  quoteText: this._quoteText.trim(),
@@ -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: "/dash/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: "/dash/settings/general/seo",
207
207
  data: { noindex: this._noindex ? "" : "true" },
208
208
  section: "seo",
209
209
  },
@@ -51,11 +51,14 @@ export interface NavManagerLabels {
51
51
  addLink: string;
52
52
  addLinkDescription: string;
53
53
  allPagesInNav: string;
54
+ createPage: string;
54
55
  urlPlaceholder: string;
55
56
  labelAndUrlRequired: string;
56
57
  maxVisibleLinks: string;
58
+ maxVisibleLinksDescription: string;
57
59
  maxVisibleSaved: string;
58
60
  useFeaturedAsDefault: string;
61
+ useFeaturedAsDefaultDescription: string;
59
62
  homeViewSaved: string;
60
63
  latest: string;
61
64
  featured: string;
@@ -113,12 +113,9 @@ export function renderPostForm(component: JantPostForm) {
113
113
 
114
114
  <div class="field">
115
115
  <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>
116
+ <div
117
+ class="post-form-tiptap-body compose-tiptap-body border rounded-lg p-3 min-h-32"
118
+ ></div>
122
119
  </div>
123
120
 
124
121
  <div class="field">
@@ -173,33 +170,40 @@ export function renderPostForm(component: JantPostForm) {
173
170
  </select>
174
171
  </div>
175
172
 
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>
173
+ <div class="field">
174
+ <label class="label">${component.labels.visibilityLabel}</label>
175
+ <select
176
+ class="select"
177
+ .value=${component._visibility}
178
+ @change=${(e: Event) => {
179
+ const target = e.target as HTMLSelectElement;
180
+ component._visibility =
181
+ (target.value as typeof component._visibility) ?? "listed";
182
+ }}
183
+ >
184
+ <option value="listed">${component.labels.visibilityListed}</option>
185
+ <option value="featured">
186
+ ${component.labels.visibilityFeatured}
187
+ </option>
188
+ <option value="unlisted">
189
+ ${component.labels.visibilityUnlisted}
190
+ </option>
191
+ </select>
201
192
  </div>
202
193
 
194
+ <label class="flex items-center gap-2 text-sm">
195
+ <input
196
+ type="checkbox"
197
+ class="checkbox"
198
+ .checked=${component._pinned}
199
+ @change=${(e: Event) => {
200
+ const target = e.target as HTMLInputElement;
201
+ component._pinned = target.checked;
202
+ }}
203
+ />
204
+ ${component.labels.pinnedLabel}
205
+ </label>
206
+
203
207
  ${renderCollections(component)}
204
208
 
205
209
  <div class="flex gap-2">
@@ -4,6 +4,7 @@
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
8
 
8
9
  export interface PostFormLabels {
9
10
  formatLabel: string;
@@ -25,7 +26,10 @@ export interface PostFormLabels {
25
26
  statusLabel: string;
26
27
  statusPublished: string;
27
28
  statusDraft: string;
28
- featuredLabel: string;
29
+ visibilityLabel: string;
30
+ visibilityListed: string;
31
+ visibilityFeatured: string;
32
+ visibilityUnlisted: string;
29
33
  pinnedLabel: string;
30
34
  collectionsLabel: string;
31
35
  submitLabel: string;
@@ -44,7 +48,7 @@ export interface PostFormInitial {
44
48
  url: string;
45
49
  quoteText: string;
46
50
  status: PostStatus;
47
- featured: boolean;
51
+ visibility: PostVisibility;
48
52
  pinned: boolean;
49
53
  rating: number;
50
54
  collectionIds: number[];
@@ -72,7 +76,7 @@ export interface PostSubmitDetail {
72
76
  title?: string;
73
77
  body?: string;
74
78
  status: PostStatus;
75
- featured: boolean;
79
+ visibility: PostVisibility;
76
80
  pinned: boolean;
77
81
  url?: string;
78
82
  quoteText?: string;