@jant/core 0.3.36 → 0.3.38

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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Dashboard Post Form
2
+ * Post Form
3
3
  *
4
4
  * Light DOM Lit component that manages post create/edit form state.
5
5
  * Dispatches `jant:post-submit` for the bridge to handle networking.
@@ -23,11 +23,12 @@ import { createTiptapEditor } from "../tiptap/create-editor.js";
23
23
  const DEFAULT_INITIAL: PostFormInitial = {
24
24
  format: "note",
25
25
  title: "",
26
+ slug: "",
26
27
  body: "",
27
28
  url: "",
28
29
  quoteText: "",
29
30
  status: "published",
30
- visibility: "listed",
31
+ visibility: "public",
31
32
  pinned: false,
32
33
  rating: 0,
33
34
  collectionIds: [],
@@ -41,6 +42,9 @@ const EMPTY_LABELS: PostFormLabels = {
41
42
  quoteOption: "",
42
43
  titleLabel: "",
43
44
  titlePlaceholder: "",
45
+ slugLabel: "",
46
+ slugPlaceholder: "",
47
+ slugHelp: "",
44
48
  bodyLabel: "",
45
49
  bodyPlaceholder: "",
46
50
  urlLabel: "",
@@ -55,8 +59,7 @@ const EMPTY_LABELS: PostFormLabels = {
55
59
  statusPublished: "",
56
60
  statusDraft: "",
57
61
  visibilityLabel: "",
58
- visibilityListed: "",
59
- visibilityFeatured: "",
62
+ visibilityPublic: "",
60
63
  visibilityUnlisted: "",
61
64
  pinnedLabel: "",
62
65
  collectionsLabel: "",
@@ -67,6 +70,7 @@ const EMPTY_LABELS: PostFormLabels = {
67
70
  mediaDialogLoading: "",
68
71
  submitSuccessMessage: "",
69
72
  submitErrorMessage: "",
73
+ draftFallbackMessage: "",
70
74
  };
71
75
 
72
76
  function parseJson<T>(value: unknown, fallback: T): T {
@@ -92,9 +96,12 @@ export class JantPostForm extends LitElement {
92
96
  action: { type: String },
93
97
  cancelHref: { type: String, attribute: "cancel-href" },
94
98
  mediaPickerUrl: { type: String, attribute: "media-picker-url" },
99
+ siteUrl: { type: String, attribute: "site-url" },
95
100
  isEdit: { type: Boolean, attribute: "is-edit" },
96
101
  _format: { state: true },
97
102
  _title: { state: true },
103
+ _slug: { state: true },
104
+ _slugManuallyEdited: { state: true },
98
105
  _body: { state: true },
99
106
  _url: { state: true },
100
107
  _quoteText: { state: true },
@@ -114,9 +121,12 @@ export class JantPostForm extends LitElement {
114
121
  declare action: string;
115
122
  declare cancelHref: string;
116
123
  declare mediaPickerUrl: string;
124
+ declare siteUrl: string;
117
125
  declare isEdit: boolean;
118
126
  declare _format: PostFormat;
119
127
  declare _title: string;
128
+ declare _slug: string;
129
+ declare _slugManuallyEdited: boolean;
120
130
  declare _body: string;
121
131
  declare _url: string;
122
132
  declare _quoteText: string;
@@ -131,6 +141,13 @@ export class JantPostForm extends LitElement {
131
141
  _editor: Editor | null = null;
132
142
  _bodyJson: JSONContent | null = null;
133
143
  #initialized = false;
144
+ #dirty = false;
145
+ #onBeforeUnload = (e: globalThis.BeforeUnloadEvent) => {
146
+ if (this.#dirty) {
147
+ e.preventDefault();
148
+ e.returnValue = "";
149
+ }
150
+ };
134
151
 
135
152
  createRenderRoot() {
136
153
  this.innerHTML = "";
@@ -144,16 +161,19 @@ export class JantPostForm extends LitElement {
144
161
  this.collections = [];
145
162
  this.media = [];
146
163
  this.action = "";
147
- this.cancelHref = "/dash/posts";
148
- this.mediaPickerUrl = "/dash/media/picker";
164
+ this.cancelHref = "/";
165
+ this.mediaPickerUrl = "";
166
+ this.siteUrl = "";
149
167
  this.isEdit = false;
150
168
  this._format = "note";
151
169
  this._title = "";
170
+ this._slug = "";
171
+ this._slugManuallyEdited = false;
152
172
  this._body = "";
153
173
  this._url = "";
154
174
  this._quoteText = "";
155
175
  this._status = "published";
156
- this._visibility = "listed";
176
+ this._visibility = "public";
157
177
  this._pinned = false;
158
178
  this._rating = 0;
159
179
  this._collectionIds = [];
@@ -203,8 +223,14 @@ export class JantPostForm extends LitElement {
203
223
  return this.querySelector("#post-media-picker");
204
224
  }
205
225
 
226
+ connectedCallback() {
227
+ super.connectedCallback();
228
+ window.addEventListener("beforeunload", this.#onBeforeUnload);
229
+ }
230
+
206
231
  disconnectedCallback() {
207
232
  super.disconnectedCallback();
233
+ window.removeEventListener("beforeunload", this.#onBeforeUnload);
208
234
  this._editor?.destroy();
209
235
  this._editor = null;
210
236
  this.closeMediaPicker();
@@ -214,11 +240,13 @@ export class JantPostForm extends LitElement {
214
240
  const init = this.initial ?? DEFAULT_INITIAL;
215
241
  this._format = init.format ?? "note";
216
242
  this._title = init.title ?? "";
243
+ this._slug = init.slug ?? "";
244
+ this._slugManuallyEdited = Boolean(init.slug);
217
245
  this._body = init.body ?? "";
218
246
  this._url = init.url ?? "";
219
247
  this._quoteText = init.quoteText ?? "";
220
248
  this._status = init.status ?? "published";
221
- this._visibility = init.visibility ?? "listed";
249
+ this._visibility = init.visibility ?? "public";
222
250
  this._pinned = !!init.pinned;
223
251
  this._rating = init.rating ?? 0;
224
252
  this._collectionIds = [...(init.collectionIds ?? [])];
@@ -253,18 +281,69 @@ export class JantPostForm extends LitElement {
253
281
  });
254
282
  }
255
283
 
284
+ /** Content-relevant properties for dirty tracking */
285
+ static #CONTENT_PROPS = new Set([
286
+ "_format",
287
+ "_title",
288
+ "_slug",
289
+ "_body",
290
+ "_url",
291
+ "_quoteText",
292
+ "_status",
293
+ "_visibility",
294
+ "_pinned",
295
+ "_rating",
296
+ "_collectionIds",
297
+ "_mediaIds",
298
+ ]);
299
+
256
300
  protected updated(changed: Map<string, unknown>) {
257
301
  super.updated(changed);
258
302
  if (!this._editor) {
259
303
  this.initEditor();
260
304
  }
305
+
306
+ // Mark dirty when user changes content after initial load
307
+ if (this.#initialized && !changed.has("initial")) {
308
+ for (const key of changed.keys()) {
309
+ if (JantPostForm.#CONTENT_PROPS.has(key as string)) {
310
+ this.#dirty = true;
311
+ break;
312
+ }
313
+ }
314
+ }
261
315
  }
262
316
 
263
- handleInput(field: "_title" | "_body" | "_url" | "_quoteText", e: Event) {
317
+ clearDirty() {
318
+ this.#dirty = false;
319
+ }
320
+
321
+ handleInput(
322
+ field: "_title" | "_slug" | "_body" | "_url" | "_quoteText",
323
+ e: Event,
324
+ ) {
264
325
  const target = e.target as HTMLInputElement | HTMLTextAreaElement;
265
326
  (this as unknown as Record<string, string>)[field] = target.value;
266
327
  }
267
328
 
329
+ handleTitleInput(e: Event) {
330
+ const target = e.target as HTMLInputElement;
331
+ this._title = target.value;
332
+ // Auto-generate slug from title if user hasn't manually edited it
333
+ if (!this._slugManuallyEdited) {
334
+ this._slug = target.value
335
+ .toLowerCase()
336
+ .replace(/[^a-z0-9]+/g, "-")
337
+ .replace(/^-|-$/g, "");
338
+ }
339
+ }
340
+
341
+ handleSlugInput(e: Event) {
342
+ const target = e.target as HTMLInputElement;
343
+ this._slug = target.value;
344
+ this._slugManuallyEdited = true;
345
+ }
346
+
268
347
  toggleCollection(id: number) {
269
348
  this._collectionIds = this._collectionIds.includes(id)
270
349
  ? this._collectionIds.filter((cid) => cid !== id)
@@ -308,6 +387,7 @@ export class JantPostForm extends LitElement {
308
387
  data: {
309
388
  format: this._format,
310
389
  title: this._title.trim(),
390
+ slug: this._slug.trim() || undefined,
311
391
  body: bodyValue,
312
392
  status: this._status,
313
393
  visibility: this._visibility,