@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.35",
3
+ "version": "0.3.36",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,16 +23,25 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@aws-sdk/client-s3": "^3.987.0",
26
+ "@emoji-mart/data": "^1.2.1",
26
27
  "@lingui/core": "^5.9.0",
27
28
  "@lingui/react": "^5.9.0",
28
29
  "@tailwindcss/typography": "^0.5.19",
30
+ "@tiptap/core": "^3.20.0",
31
+ "@tiptap/extension-image": "^3.20.0",
32
+ "@tiptap/extension-placeholder": "^3.20.0",
33
+ "@tiptap/extension-table": "^3.20.0",
34
+ "@tiptap/pm": "^3.20.0",
35
+ "@tiptap/starter-kit": "^3.20.0",
36
+ "@tiptap/suggestion": "^3.20.0",
29
37
  "basecoat-css": "^0.3.10",
30
38
  "better-auth": "^1.4.18",
31
39
  "drizzle-orm": "^0.45.1",
40
+ "emoji-mart": "^5.6.0",
41
+ "limax": "^4.2.2",
32
42
  "lit": "^3.3.2",
33
43
  "lucide-static": "^0.574.0",
34
44
  "marked": "^17.0.1",
35
- "pinyin-pro": "^3.28.0",
36
45
  "sortablejs": "^1.15.6",
37
46
  "sqids": "^0.3.0",
38
47
  "uuidv7": "^1.1.0",
@@ -50,13 +59,13 @@
50
59
  "@lingui/format-po": "^5.9.0",
51
60
  "@lingui/swc-plugin": "^5.10.1",
52
61
  "@swc/core": "^1.15.11",
62
+ "@tailwindcss/vite": "^4.1.18",
53
63
  "@types/better-sqlite3": "^7.6.13",
54
64
  "@types/node": "^25.1.0",
55
65
  "better-sqlite3": "^12.6.2",
56
66
  "drizzle-kit": "^0.31.8",
57
67
  "glob": "^13.0.0",
58
68
  "happy-dom": "^20.6.3",
59
- "@tailwindcss/vite": "^4.1.18",
60
69
  "tailwindcss": "^4.1.18",
61
70
  "tsx": "^4.21.0",
62
71
  "typescript": "^5.9.3",
@@ -98,7 +107,7 @@
98
107
  "i18n:compile": "lingui compile --typescript",
99
108
  "i18n:build": "pnpm i18n:extract && pnpm i18n:compile",
100
109
  "dev": "pnpm db:migrate:local && vite dev",
101
- "dev:debug": "pnpm db:migrate:local && vite dev --port 19019",
110
+ "dev:debug": "pnpm db:migrate:local && vite dev --port 19020",
102
111
  "db:migrate:local": "echo y | wrangler d1 migrations apply DB --local",
103
112
  "db:migrate:remote": "wrangler d1 migrations apply DB --remote",
104
113
  "test": "vitest run",
@@ -69,7 +69,7 @@ export function createTestApp(options: TestAppOptions = {}) {
69
69
  app.use("*", async (c, next) => {
70
70
  // Provide mock env bindings so c.env.* works in route handlers
71
71
  c.env = {
72
- SITE_URL: "http://localhost:9019",
72
+ SITE_URL: "http://localhost:9020",
73
73
  } as AppVariables["services"] extends never ? never : Bindings;
74
74
 
75
75
  c.set("services", services as AppVariables["services"]);
@@ -122,6 +122,12 @@ export function createTestDatabase(options?: { fts?: boolean }) {
122
122
  // Apply 0011: path registry
123
123
  applyMigration(sqlite, "0011_add_path_registry.sql");
124
124
 
125
+ // Apply 0012: Tiptap columns (summary)
126
+ applyMigration(sqlite, "0012_add_tiptap_columns.sql");
127
+
128
+ // Apply 0013: Replace featured with visibility
129
+ applyMigration(sqlite, "0013_replace_featured_with_visibility.sql");
130
+
125
131
  const db = drizzle(sqlite, { schema });
126
132
 
127
133
  // Polyfill D1 batch() for test compatibility.
package/src/app.tsx CHANGED
@@ -32,8 +32,6 @@ import { pagesRoutes as dashPagesRoutes } from "./routes/dash/pages.js";
32
32
  import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
33
33
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
34
34
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
35
- import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
36
- import { appearanceRoutes as dashAppearanceRoutes } from "./routes/dash/appearance.js";
37
35
 
38
36
  // Routes - API
39
37
  import { postsApiRoutes } from "./routes/api/posts.js";
@@ -247,10 +245,8 @@ export function createApp(): App {
247
245
  app.route("/dash/pages", dashPagesRoutes);
248
246
  app.route("/dash/media", dashMediaRoutes);
249
247
  app.route("/dash/settings", dashSettingsRoutes);
250
- app.route("/dash/appearance", dashAppearanceRoutes);
251
248
  app.route("/dash/settings/redirects", dashRedirectsRoutes);
252
- app.route("/dash/collections", dashCollectionsRoutes);
253
- // API routes
249
+ // Protected API routes
254
250
  app.route("/api/upload", uploadApiRoutes);
255
251
  app.route("/api/search", searchApiRoutes);
256
252
 
@@ -9,7 +9,7 @@
9
9
  * Uses the `[data-avatar-upload]` attribute on file inputs.
10
10
  */
11
11
 
12
- import { encodeIco } from "./favicon.js";
12
+ import { encodeIco } from "../lib/favicon.js";
13
13
 
14
14
  /**
15
15
  * Load an image from a File object
@@ -6,8 +6,8 @@
6
6
  * redirects on success. Displays toasts on failure.
7
7
  */
8
8
 
9
- import type { CollectionSubmitDetail } from "../ui/components/collection-types.js";
10
- import type { JantCollectionForm } from "../ui/components/jant-collection-form.js";
9
+ import type { CollectionSubmitDetail } from "./components/collection-types.js";
10
+ import type { JantCollectionForm } from "./components/jant-collection-form.js";
11
11
  import { showToast } from "./toast.js";
12
12
 
13
13
  document.addEventListener("jant:collection-submit", async (event: Event) => {
@@ -1,6 +1,20 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { describe, it, expect, beforeEach } from "vitest";
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
+
5
+ vi.mock("../../lazy-slugify.js", () => ({
6
+ slugify: (text: string) =>
7
+ Promise.resolve(
8
+ text
9
+ .toLowerCase()
10
+ .trim()
11
+ .replace(/[^\w\s-]/g, "")
12
+ .replace(/[\s_-]+/g, "-")
13
+ .replace(/^-+|-+$/g, ""),
14
+ ),
15
+ preloadSlug: () => {},
16
+ }));
17
+
4
18
  import type {
5
19
  CollectionFormInitial,
6
20
  CollectionFormLabels,
@@ -16,12 +30,11 @@ const labels: CollectionFormLabels = {
16
30
  slugHelp: "Help text",
17
31
  descriptionLabel: "Description",
18
32
  descriptionPlaceholder: "Placeholder Description",
19
- iconLabel: "Icon",
20
- chooseIcon: "Choose Icon",
21
33
  removeIcon: "Remove",
22
- dialogTitle: "Choose Icon",
23
- dialogClose: "Close",
34
+ iconsTab: "Icons",
35
+ emojisTab: "Emojis",
24
36
  searchIconsPlaceholder: "Search icons...",
37
+ searchEmojisPlaceholder: "Search emojis...",
25
38
  sortOrderLabel: "Sort Order",
26
39
  sortNewest: "Newest first",
27
40
  sortOldest: "Oldest first",
@@ -47,8 +60,8 @@ async function createElement(
47
60
  ) as JantCollectionForm;
48
61
  el.labels = labels;
49
62
  el.initial = initial;
50
- el.action = "/dash/collections";
51
- el.cancelHref = "/dash/collections";
63
+ el.action = "/api/collections";
64
+ el.cancelHref = "/api/collections";
52
65
  el.isEdit = false;
53
66
  Object.assign(el, overrides);
54
67
  document.body.appendChild(el);
@@ -82,6 +95,8 @@ describe("JantCollectionForm", () => {
82
95
 
83
96
  titleInput.value = "My Great Collection!";
84
97
  titleInput.dispatchEvent(new Event("input", { bubbles: true }));
98
+ // slugify is async — flush the microtask then wait for Lit re-render
99
+ await new Promise((r) => setTimeout(r, 0));
85
100
  await el.updateComplete;
86
101
 
87
102
  expect(slugInput.value).toBe("my-great-collection");
@@ -143,11 +158,13 @@ describe("JantCollectionForm", () => {
143
158
 
144
159
  expect(detail).not.toBeNull();
145
160
  const d = detail as unknown as CollectionSubmitDetail;
146
- expect(d.endpoint).toBe("/dash/collections");
161
+ expect(d.endpoint).toBe("/api/collections");
147
162
  expect(d.data.title).toBe("Books");
148
163
  expect(d.data.slug).toBe("books");
149
164
  expect(d.data.description).toBe("All about books");
150
165
  expect(d.data.sortOrder).toBe("rating_desc");
151
- expect(d.data.icon).toBeUndefined();
166
+ // Default icon is auto-selected in create mode (library icon with gray color)
167
+ expect(d.data.icon).toBeDefined();
168
+ expect(d.data.icon).toContain('"name":"library"');
152
169
  });
153
170
  });
@@ -42,7 +42,8 @@ const labels: ComposeLabels = {
42
42
  attachedTextHint: "Supplementary content",
43
43
  done: "Done",
44
44
  media: "Media",
45
- score: "Score",
45
+ rate: "Rate",
46
+ emoji: "Emoji",
46
47
  title: "Title",
47
48
  collection: "Collection",
48
49
  searchCollections: "Search...",
@@ -55,6 +56,7 @@ const labels: ComposeLabels = {
55
56
  addMore: "Add",
56
57
  uploading: "Uploading...",
57
58
  published: "Published!",
59
+ retryAll: "Click to retry all",
58
60
  };
59
61
 
60
62
  const collections: ComposeCollection[] = [
@@ -135,7 +137,12 @@ describe("JantComposeDialog", () => {
135
137
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
136
138
  "expected compose editor",
137
139
  );
138
- editor._body = "Hello world";
140
+ editor._bodyJson = {
141
+ type: "doc",
142
+ content: [
143
+ { type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
144
+ ],
145
+ };
139
146
  await editor.updateComplete;
140
147
 
141
148
  let receivedDetail: ComposeSubmitDetail | null = null;
@@ -153,7 +160,7 @@ describe("JantComposeDialog", () => {
153
160
  expect(receivedDetail).not.toBeNull();
154
161
  const detail = receivedDetail as unknown as ComposeSubmitDetail;
155
162
  expect(detail.format).toBe("note");
156
- expect(detail.body).toBe("Hello world");
163
+ expect(detail.body).toContain("Hello world");
157
164
  expect(detail.status).toBe("published");
158
165
  expect(detail.collectionIds).toEqual([]);
159
166
  expect(detail.mediaIds).toEqual([]);
@@ -233,7 +240,15 @@ describe("JantComposeDialog", () => {
233
240
  el.querySelector<JantComposeEditor>("jant-compose-editor"),
234
241
  "expected compose editor",
235
242
  );
236
- editor._body = "Draft content";
243
+ editor._bodyJson = {
244
+ type: "doc",
245
+ content: [
246
+ {
247
+ type: "paragraph",
248
+ content: [{ type: "text", text: "Draft content" }],
249
+ },
250
+ ],
251
+ };
237
252
  await editor.updateComplete;
238
253
 
239
254
  let receivedDetail: ComposeSubmitDetail | null = null;
@@ -319,10 +334,10 @@ describe("JantComposeDialog", () => {
319
334
  expect(editor.querySelector(".compose-attachment-thumb")).not.toBeNull();
320
335
  // ALT button should be visible
321
336
  expect(editor.querySelector(".compose-attachment-alt")).not.toBeNull();
322
- // Media tool button should show "Add" label
337
+ // Media tool button should show inline "Add" label
323
338
  const mediaBtn =
324
339
  editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
325
- expect(mediaBtn?.querySelector(".compose-tool-tip")?.textContent).toBe(
340
+ expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
326
341
  "Add",
327
342
  );
328
343
 
@@ -397,20 +412,21 @@ describe("JantComposeDialog", () => {
397
412
  );
398
413
  altBtn.click();
399
414
  await editor.updateComplete;
415
+ await el.updateComplete;
400
416
 
401
- // Alt panel should be visible
402
- expect(editor.querySelector(".compose-alt-panel")).not.toBeNull();
417
+ // Alt panel should be visible in the dialog (covers entire dialog)
418
+ expect(el.querySelector(".compose-alt-panel")).not.toBeNull();
403
419
  expect(editor._showAltPanel).toBe(true);
404
420
 
405
421
  // Click done to close
406
- const doneBtn = editor.querySelector<HTMLButtonElement>(
422
+ const doneBtn = el.querySelector<HTMLButtonElement>(
407
423
  ".compose-alt-panel .compose-post-btn",
408
424
  );
409
425
  doneBtn?.click();
410
- await editor.updateComplete;
426
+ await el.updateComplete;
411
427
 
412
- expect(editor._showAltPanel).toBe(false);
413
- expect(editor.querySelector(".compose-alt-panel")).toBeNull();
428
+ expect(editor._showAltPanel).toBe(true); // Editor still tracks its own state
429
+ expect(el.querySelector(".compose-alt-panel")).toBeNull();
414
430
 
415
431
  URL.revokeObjectURL(previewUrl);
416
432
  });
@@ -437,7 +453,15 @@ describe("JantComposeDialog", () => {
437
453
  error: null,
438
454
  },
439
455
  ];
440
- editor._body = "Post with image";
456
+ editor._bodyJson = {
457
+ type: "doc",
458
+ content: [
459
+ {
460
+ type: "paragraph",
461
+ content: [{ type: "text", text: "Post with image" }],
462
+ },
463
+ ],
464
+ };
441
465
  await editor.updateComplete;
442
466
 
443
467
  let receivedDetail: ComposeSubmitDetail | null = null;
@@ -481,7 +505,15 @@ describe("JantComposeDialog", () => {
481
505
  error: null,
482
506
  },
483
507
  ];
484
- editor._body = "Post with pending upload";
508
+ editor._bodyJson = {
509
+ type: "doc",
510
+ content: [
511
+ {
512
+ type: "paragraph",
513
+ content: [{ type: "text", text: "Post with pending upload" }],
514
+ },
515
+ ],
516
+ };
485
517
  await editor.updateComplete;
486
518
 
487
519
  let deferredEvent: CustomEvent | null = null;
@@ -48,7 +48,8 @@ const labels: ComposeLabels = {
48
48
  attachedTextHint: "Supplementary content",
49
49
  done: "Done",
50
50
  media: "Media",
51
- score: "Score",
51
+ rate: "Rate",
52
+ emoji: "Emoji",
52
53
  title: "Title",
53
54
  collection: "Collection",
54
55
  searchCollections: "Search...",
@@ -61,6 +62,7 @@ const labels: ComposeLabels = {
61
62
  addMore: "Add",
62
63
  uploading: "Uploading...",
63
64
  published: "Published!",
65
+ retryAll: "Click to retry all",
64
66
  };
65
67
 
66
68
  async function createElement(
@@ -81,11 +83,11 @@ describe("JantComposeEditor", () => {
81
83
 
82
84
  it("renders note fields by default", async () => {
83
85
  const el = await createElement("note");
84
- const textarea = requireElement(
85
- el.querySelector<HTMLTextAreaElement>(".compose-body-input"),
86
- "expected compose body textarea",
86
+ const tiptapContainer = requireElement(
87
+ el.querySelector<HTMLElement>(".compose-tiptap-body"),
88
+ "expected compose Tiptap body container",
87
89
  );
88
- expect(textarea.placeholder).toBe("What's on your mind...");
90
+ expect(tiptapContainer).toBeTruthy();
89
91
  });
90
92
 
91
93
  it("renders link fields when format is link", async () => {
@@ -158,9 +160,12 @@ describe("JantComposeEditor", () => {
158
160
  expect(el._rating).toBe(0);
159
161
  });
160
162
 
161
- it("toggles attached text panel", async () => {
163
+ it("dispatches attached panel open event", async () => {
162
164
  const el = await createElement("note");
163
165
 
166
+ const events: Event[] = [];
167
+ el.addEventListener("jant:attached-panel-open", (e) => events.push(e));
168
+
164
169
  // Click attached text tool button
165
170
  const toolBtns =
166
171
  el.querySelectorAll<HTMLButtonElement>(".compose-tool-btn");
@@ -172,16 +177,8 @@ describe("JantComposeEditor", () => {
172
177
  attachedBtn.click();
173
178
  await el.updateComplete;
174
179
 
175
- expect(el.querySelector(".compose-attached-panel")).not.toBeNull();
176
-
177
- // Click done button to close
178
- const doneBtn = el.querySelector<HTMLButtonElement>(
179
- ".compose-attached-panel .compose-post-btn",
180
- );
181
- doneBtn?.click();
182
- await el.updateComplete;
183
-
184
- expect(el.querySelector(".compose-attached-panel")).toBeNull();
180
+ expect(events).toHaveLength(1);
181
+ expect(el._showAttachedText).toBe(true);
185
182
  });
186
183
 
187
184
  it("shows title toggle only in note mode", async () => {
@@ -197,13 +194,19 @@ describe("JantComposeEditor", () => {
197
194
  it("getData returns current field values", async () => {
198
195
  const el = await createElement("note");
199
196
  el._title = "Test Title";
200
- el._body = "Test Body";
197
+ el._showTitle = true;
198
+ el._bodyJson = {
199
+ type: "doc",
200
+ content: [
201
+ { type: "paragraph", content: [{ type: "text", text: "Test Body" }] },
202
+ ],
203
+ };
201
204
  el._rating = 4;
202
205
  el._attachedText = "Some attached text";
203
206
 
204
207
  const data = el.getData();
205
208
  expect(data.title).toBe("Test Title");
206
- expect(data.body).toBe("Test Body");
209
+ expect(data.body).toContain("Test Body");
207
210
  expect(data.rating).toBe(4);
208
211
  expect(data.attachedText).toBe("Some attached text");
209
212
  expect(data.url).toBe("");
@@ -211,10 +214,42 @@ describe("JantComposeEditor", () => {
211
214
  expect(data.quoteAuthor).toBe("");
212
215
  });
213
216
 
217
+ it("getData omits title when showTitle is off in note mode", async () => {
218
+ const el = await createElement("note");
219
+ el._title = "Hidden Title";
220
+ el._showTitle = false;
221
+
222
+ const data = el.getData();
223
+ expect(data.title).toBe("");
224
+ });
225
+
226
+ it("preserves title in memory when toggling off and restores on toggle on", async () => {
227
+ const el = await createElement("note");
228
+ el._title = "My Title";
229
+ el._showTitle = true;
230
+ await el.updateComplete;
231
+
232
+ // Toggle off — title stays in memory
233
+ el._showTitle = false;
234
+ await el.updateComplete;
235
+ expect(el._title).toBe("My Title");
236
+ expect(el.getData().title).toBe("");
237
+
238
+ // Toggle back on — title restored
239
+ el._showTitle = true;
240
+ await el.updateComplete;
241
+ expect(el.getData().title).toBe("My Title");
242
+ });
243
+
214
244
  it("reset clears all fields", async () => {
215
245
  const el = await createElement("note");
216
246
  el._title = "Test";
217
- el._body = "Body";
247
+ el._bodyJson = {
248
+ type: "doc",
249
+ content: [
250
+ { type: "paragraph", content: [{ type: "text", text: "Body" }] },
251
+ ],
252
+ };
218
253
  el._rating = 3;
219
254
  el._showRating = true;
220
255
  el._attachedText = "text";
@@ -223,7 +258,7 @@ describe("JantComposeEditor", () => {
223
258
  el.reset();
224
259
 
225
260
  expect(el._title).toBe("");
226
- expect(el._body).toBe("");
261
+ expect(el._bodyJson).toBeNull();
227
262
  expect(el._rating).toBe(0);
228
263
  expect(el._showRating).toBe(false);
229
264
  expect(el._attachedText).toBe("");
@@ -240,12 +275,12 @@ describe("JantComposeEditor", () => {
240
275
  expect(badge?.textContent).toContain("chars");
241
276
  });
242
277
 
243
- it("media button highlights when attachments are present", async () => {
278
+ it("media button shows inline add label when attachments are present", async () => {
244
279
  const el = await createElement("note");
245
280
 
246
- // Media button should not be active initially
281
+ // Media button should not have add style initially
247
282
  const mediaBtn = el.querySelector<HTMLButtonElement>(".compose-tool-btn");
248
- expect(mediaBtn?.classList.contains("compose-tool-btn-active")).toBe(false);
283
+ expect(mediaBtn?.classList.contains("compose-tool-btn-add")).toBe(false);
249
284
 
250
285
  // Add an attachment
251
286
  const blob = new Blob(["fake"], { type: "image/png" });
@@ -265,8 +300,13 @@ describe("JantComposeEditor", () => {
265
300
 
266
301
  const mediaBtnAfter =
267
302
  el.querySelector<HTMLButtonElement>(".compose-tool-btn");
268
- expect(mediaBtnAfter?.classList.contains("compose-tool-btn-active")).toBe(
303
+ expect(mediaBtnAfter?.classList.contains("compose-tool-btn-add")).toBe(
269
304
  true,
270
305
  );
306
+
307
+ // Should show inline label, not tooltip
308
+ const label = mediaBtnAfter?.querySelector(".compose-tool-label");
309
+ expect(label).not.toBeNull();
310
+ expect(label?.textContent).toBe("Add");
271
311
  });
272
312
  });
@@ -31,7 +31,10 @@ const labels: PostFormLabels = {
31
31
  statusLabel: "Status",
32
32
  statusPublished: "Published",
33
33
  statusDraft: "Draft",
34
- featuredLabel: "Featured",
34
+ visibilityLabel: "Visibility",
35
+ visibilityListed: "Listed",
36
+ visibilityFeatured: "Featured",
37
+ visibilityUnlisted: "Unlisted",
35
38
  pinnedLabel: "Pinned",
36
39
  collectionsLabel: "Collections",
37
40
  submitLabel: "Publish",
@@ -50,7 +53,7 @@ const initial: PostFormInitial = {
50
53
  url: "",
51
54
  quoteText: "",
52
55
  status: "published",
53
- featured: false,
56
+ visibility: "listed",
54
57
  pinned: false,
55
58
  rating: 0,
56
59
  collectionIds: [],
@@ -119,21 +122,28 @@ describe("JantPostForm", () => {
119
122
  titleInput.value = "Sample Post";
120
123
  titleInput.dispatchEvent(new Event("input", { bubbles: true }));
121
124
 
122
- const bodyTextarea =
123
- el.querySelector<HTMLTextAreaElement>("textarea.textarea");
124
- expect(bodyTextarea).not.toBeNull();
125
- if (!bodyTextarea) throw new Error("Body textarea not found");
126
- bodyTextarea.value = "Hello world";
127
- bodyTextarea.dispatchEvent(new Event("input", { bubbles: true }));
125
+ // Set body via Tiptap JSON state (Tiptap editor may not init in happy-dom)
126
+ el._bodyJson = {
127
+ type: "doc",
128
+ content: [
129
+ { type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
130
+ ],
131
+ };
132
+ el._body = JSON.stringify(el._bodyJson);
133
+
134
+ // Set visibility to "featured" via the select dropdown
135
+ const visibilitySelect =
136
+ el.querySelectorAll<HTMLSelectElement>("select.select")[2]; // [0]=format, [1]=status, [2]=visibility
137
+ expect(visibilitySelect).not.toBeNull();
138
+ if (!visibilitySelect) throw new Error("Visibility select not found");
139
+ visibilitySelect.value = "featured";
140
+ visibilitySelect.dispatchEvent(new Event("change", { bubbles: true }));
128
141
 
129
142
  const checkboxList =
130
143
  el.querySelectorAll<HTMLInputElement>("input.checkbox");
131
144
  expect(checkboxList.length).toBeGreaterThan(0);
132
- const checkbox = checkboxList[0];
133
- checkbox.checked = true;
134
- checkbox.dispatchEvent(new Event("change", { bubbles: true }));
135
145
 
136
- const collectionCheckbox = checkboxList.item(2);
146
+ const collectionCheckbox = checkboxList.item(1);
137
147
  expect(collectionCheckbox).not.toBeNull();
138
148
  if (!collectionCheckbox) throw new Error("Collection checkbox missing");
139
149
  collectionCheckbox.checked = true;
@@ -154,8 +164,8 @@ describe("JantPostForm", () => {
154
164
  const d = detail as unknown as PostSubmitDetail;
155
165
  expect(d.endpoint).toBe("/dash/posts");
156
166
  expect(d.data.title).toBe("Sample Post");
157
- expect(d.data.body).toBe("Hello world");
158
- expect(d.data.featured).toBe(true);
167
+ expect(d.data.body).toContain("Hello world");
168
+ expect(d.data.visibility).toBe("featured");
159
169
  expect(d.data.collectionIds).toEqual([collections[0].id]);
160
170
  expect(d.data.mediaIds).toEqual(["m1"]);
161
171
  });
@@ -188,7 +188,7 @@ describe("JantSettingsGeneral", () => {
188
188
 
189
189
  expect(detail).not.toBeNull();
190
190
  const d = detail as unknown as SettingsSaveDetail;
191
- expect(d.endpoint).toBe("/dash/settings");
191
+ expect(d.endpoint).toBe("/dash/settings/general");
192
192
  expect(d.section).toBe("general");
193
193
  expect(d.data.siteName).toBe("New Name");
194
194
  });
@@ -255,7 +255,7 @@ describe("JantSettingsGeneral", () => {
255
255
 
256
256
  expect(detail).not.toBeNull();
257
257
  const d = detail as unknown as SettingsSaveDetail;
258
- expect(d.endpoint).toBe("/dash/settings");
258
+ expect(d.endpoint).toBe("/dash/settings/general");
259
259
  expect(d.section).toBe("general");
260
260
  expect(d.data.siteFooter).toBe("New footer");
261
261
  });
@@ -286,7 +286,7 @@ describe("JantSettingsGeneral", () => {
286
286
 
287
287
  expect(detail).not.toBeNull();
288
288
  const d = detail as unknown as SettingsSaveDetail;
289
- expect(d.endpoint).toBe("/dash/settings/seo");
289
+ expect(d.endpoint).toBe("/dash/settings/general/seo");
290
290
  expect(d.section).toBe("seo");
291
291
  });
292
292
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Type definitions for the collection sidebar Lit component.
3
+ */
4
+
5
+ import type { CollectionFormLabels } from "./collection-types.js";
6
+
7
+ export interface CollectionSidebarLabels {
8
+ collections: string;
9
+ reorder: string;
10
+ done: string;
11
+ addDivider: string;
12
+ newCollection: string;
13
+ edit: string;
14
+ deleteDivider: string;
15
+ moreActions: string;
16
+ deleteCollection: string;
17
+ confirmDelete: string;
18
+ // Toast messages
19
+ orderSaved: string;
20
+ saved: string;
21
+ saveFailed: string;
22
+ deleted: string;
23
+ // Collection form labels (passed through to jant-collection-form)
24
+ formLabels: CollectionFormLabels;
25
+ }
26
+
27
+ export interface SidebarCollection {
28
+ id: number;
29
+ slug: string;
30
+ title: string;
31
+ description: string | null;
32
+ icon: string | null;
33
+ sortOrder: string;
34
+ position: number;
35
+ postCount: number;
36
+ }
37
+
38
+ export interface SidebarDivider {
39
+ id: number;
40
+ position: number;
41
+ }
42
+
43
+ export type SidebarItem =
44
+ | { kind: "collection"; data: SidebarCollection }
45
+ | { kind: "divider"; data: SidebarDivider };
@@ -9,12 +9,11 @@ export interface CollectionFormLabels {
9
9
  slugHelp: string;
10
10
  descriptionLabel: string;
11
11
  descriptionPlaceholder: string;
12
- iconLabel: string;
13
- chooseIcon: string;
14
12
  removeIcon: string;
15
- dialogTitle: string;
16
- dialogClose: string;
13
+ iconsTab: string;
14
+ emojisTab: string;
17
15
  searchIconsPlaceholder: string;
16
+ searchEmojisPlaceholder: string;
18
17
  sortOrderLabel: string;
19
18
  sortNewest: string;
20
19
  sortOldest: string;