@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.
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
253
|
-
// API routes
|
|
249
|
+
// Protected API routes
|
|
254
250
|
app.route("/api/upload", uploadApiRoutes);
|
|
255
251
|
app.route("/api/search", searchApiRoutes);
|
|
256
252
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* redirects on success. Displays toasts on failure.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { CollectionSubmitDetail } from "
|
|
10
|
-
import type { JantCollectionForm } from "
|
|
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
|
-
|
|
23
|
-
|
|
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 = "/
|
|
51
|
-
el.cancelHref = "/
|
|
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("/
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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).
|
|
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.
|
|
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-
|
|
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(
|
|
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 =
|
|
422
|
+
const doneBtn = el.querySelector<HTMLButtonElement>(
|
|
407
423
|
".compose-alt-panel .compose-post-btn",
|
|
408
424
|
);
|
|
409
425
|
doneBtn?.click();
|
|
410
|
-
await
|
|
426
|
+
await el.updateComplete;
|
|
411
427
|
|
|
412
|
-
expect(editor._showAltPanel).toBe(
|
|
413
|
-
expect(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
85
|
-
el.querySelector<
|
|
86
|
-
"expected compose body
|
|
86
|
+
const tiptapContainer = requireElement(
|
|
87
|
+
el.querySelector<HTMLElement>(".compose-tiptap-body"),
|
|
88
|
+
"expected compose Tiptap body container",
|
|
87
89
|
);
|
|
88
|
-
expect(
|
|
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("
|
|
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(
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
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).
|
|
158
|
-
expect(d.data.
|
|
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
|
-
|
|
16
|
-
|
|
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;
|