@jant/core 0.6.12 → 0.6.14
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/bin/commands/import-site.js +2 -10
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/{app-D-zAhayc.js → app-2i7-WQHg.js} +426 -450
- package/dist/app-DZDlmh7C.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-S1cdvulk.js → client-BvID5ucz.js} +1 -1
- package/dist/client/_assets/client-Cevfpm8H.css +2 -0
- package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-v3yvhbgZ.js} +596 -590
- package/dist/{export-Ckwh4BiE.js → export-DWn149b7.js} +24 -19
- package/dist/{github-sync-BMmK3JZ0.js → github-sync-C0zyF6XS.js} +2 -2
- package/dist/{github-sync-Dl9wQMar.js → github-sync-Ckk0sJxK.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +1 -2
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
- package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +100 -70
- package/src/client/components/jant-compose-dialog.ts +3 -1
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +245 -155
- package/src/client/components/nav-manager-types.ts +0 -10
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
- package/src/client/tiptap/bubble-menu.ts +3 -3
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/markdown-clipboard.ts +7 -2
- package/src/client-auth.ts +0 -1
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +20 -4
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +20 -4
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +23 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +23 -20
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +23 -20
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/markdown-manager.ts +14 -5
- package/src/lib/navigation.ts +5 -22
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +8 -2
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +41 -0
- package/src/services/__tests__/post-timeline.test.ts +56 -0
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +5 -15
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/navigation.ts +44 -13
- package/src/services/post.ts +80 -57
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/tokens.css +2 -2
- package/src/styles/ui.css +316 -34
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
- package/src/ui/layouts/BaseLayout.tsx +1 -1
- package/src/ui/layouts/SiteLayout.tsx +266 -227
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/dist/app-B5P1b4TF.js +0 -6
- package/dist/client/_assets/client-BUxR-E8O.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import "./export-
|
|
3
|
-
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-
|
|
2
|
+
import "./export-DWn149b7.js";
|
|
3
|
+
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-Ckk0sJxK.js";
|
|
4
4
|
export { classifyRepoForSync, createGitHubSyncService };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-
|
|
1
|
+
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-DWn149b7.js";
|
|
2
2
|
import { r as getInstallationToken } from "./github-app-BbklkFmU.js";
|
|
3
3
|
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BgSiE71w.js";
|
|
4
4
|
//#region src/lib/markdown-to-tiptap.ts
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { y as url_exports } from "./url-BMYO-Zlt.js";
|
|
2
|
-
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-
|
|
3
|
-
import { S as time_exports, a as markdown_exports } from "./export-
|
|
2
|
+
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-2i7-WQHg.js";
|
|
3
|
+
import { S as time_exports, a as markdown_exports } from "./export-DWn149b7.js";
|
|
4
4
|
import "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync-Ckk0sJxK.js";
|
|
6
6
|
export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
|
package/dist/node.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-
|
|
3
|
-
import { t as createExportService } from "./export-
|
|
2
|
+
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-2i7-WQHg.js";
|
|
3
|
+
import { t as createExportService } from "./export-DWn149b7.js";
|
|
4
4
|
import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync-Ckk0sJxK.js";
|
|
6
6
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
7
|
import { serve } from "@hono/node-server";
|
|
8
8
|
import Database from "better-sqlite3";
|
|
@@ -529,7 +529,7 @@ async function createNodeRequestHandler(options) {
|
|
|
529
529
|
async function start(env = process.env, app) {
|
|
530
530
|
const handler = await createNodeRequestHandler({
|
|
531
531
|
env,
|
|
532
|
-
app: async () => app ?? (await import("./app-
|
|
532
|
+
app: async () => app ?? (await import("./app-DZDlmh7C.js")).createApp()
|
|
533
533
|
});
|
|
534
534
|
const hostname = resolveHost(env);
|
|
535
535
|
const port = resolvePort(env);
|
package/package.json
CHANGED
|
@@ -281,7 +281,6 @@ describe("createExportService (Hugo)", () => {
|
|
|
281
281
|
makeSiteConfig({
|
|
282
282
|
siteName: "My Site",
|
|
283
283
|
siteUrl: "https://my.example",
|
|
284
|
-
homeDefaultView: "featured",
|
|
285
284
|
}),
|
|
286
285
|
);
|
|
287
286
|
const files = filesToMap(await service.generateHugoFiles());
|
|
@@ -292,7 +291,7 @@ describe("createExportService (Hugo)", () => {
|
|
|
292
291
|
expect(toml).toContain('theme = "jant"');
|
|
293
292
|
expect(toml).toMatch(/\[permalinks\][\s\S]*post = "\/:slug\/"/);
|
|
294
293
|
expect(toml).toContain("[params]");
|
|
295
|
-
expect(toml).toContain(
|
|
294
|
+
expect(toml).not.toContain("home_default_view");
|
|
296
295
|
});
|
|
297
296
|
|
|
298
297
|
it("configures hugo.toml for Atom RSS output with per-section opt-in", async () => {
|
|
@@ -123,6 +123,9 @@ export function createTestApp(options: TestAppOptions = {}) {
|
|
|
123
123
|
c.set("appConfig", resolveConfig(c.env, allSettings));
|
|
124
124
|
c.set("storage", options.storage ?? null);
|
|
125
125
|
c.set("rateLimiter", rateLimiter);
|
|
126
|
+
const publicRequestUrl = c.req.url;
|
|
127
|
+
c.set("publicRequestUrl", publicRequestUrl);
|
|
128
|
+
c.set("publicPath", new URL(publicRequestUrl).pathname);
|
|
126
129
|
|
|
127
130
|
// i18n (English default for tests)
|
|
128
131
|
const i18n = createI18n("en");
|
|
@@ -88,7 +88,6 @@ export function makeSiteConfig(over: Partial<SiteConfig> = {}): SiteConfig {
|
|
|
88
88
|
siteDescription: "Export fixture",
|
|
89
89
|
siteLanguage: "en",
|
|
90
90
|
showJantBrandingOnHome: true,
|
|
91
|
-
homeDefaultView: "latest",
|
|
92
91
|
mainRssFeed: "latest",
|
|
93
92
|
siteFooter: "",
|
|
94
93
|
showHeaderAvatar: false,
|
|
@@ -98,7 +98,6 @@ describe("Hugo import CLI helpers", () => {
|
|
|
98
98
|
'site_name = "Example Site"',
|
|
99
99
|
'site_description = "A description"',
|
|
100
100
|
'site_language = "en"',
|
|
101
|
-
'home_default_view = "featured"',
|
|
102
101
|
"show_jant_branding_on_home = true",
|
|
103
102
|
"show_header_avatar = false",
|
|
104
103
|
"noindex = false",
|
|
@@ -136,7 +135,7 @@ describe("Hugo import CLI helpers", () => {
|
|
|
136
135
|
expect(siteConfig.title).toBe("Example Site");
|
|
137
136
|
expect(siteConfig.base_url).toBe("https://example.com/");
|
|
138
137
|
expect(siteConfig.extra.jant.theme_id).toBe("paper");
|
|
139
|
-
expect(siteConfig.extra.jant
|
|
138
|
+
expect(siteConfig.extra.jant).not.toHaveProperty("home_default_view");
|
|
140
139
|
expect(siteConfig.extra.jant.nav_exported).toBe(true);
|
|
141
140
|
expect(siteConfig.extra.jant.nav).toHaveLength(1);
|
|
142
141
|
expect(siteConfig.extra.jant.collections_directory_exported).toBe(true);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
type SiteHeaderInit = typeof import("../site-header-nav.js").initSiteHeaderNav;
|
|
6
|
+
type SiteHeaderDestroy =
|
|
7
|
+
typeof import("../site-header-nav.js").destroySiteHeaderNav;
|
|
6
8
|
|
|
7
9
|
function createDrawerDOM(): HTMLElement {
|
|
8
10
|
const root = document.createElement("div");
|
|
@@ -50,13 +52,15 @@ function createMoreDropdownDOM(): HTMLElement {
|
|
|
50
52
|
|
|
51
53
|
describe("site header more dropdown", () => {
|
|
52
54
|
let initSiteHeaderNav: SiteHeaderInit;
|
|
55
|
+
let destroySiteHeaderNav: SiteHeaderDestroy;
|
|
53
56
|
let root: HTMLElement;
|
|
54
57
|
|
|
55
58
|
beforeEach(async () => {
|
|
56
59
|
document.body.innerHTML = "";
|
|
57
60
|
vi.resetModules();
|
|
58
61
|
root = createMoreDropdownDOM();
|
|
59
|
-
({ initSiteHeaderNav } =
|
|
62
|
+
({ initSiteHeaderNav, destroySiteHeaderNav } =
|
|
63
|
+
await import("../site-header-nav.js"));
|
|
60
64
|
initSiteHeaderNav(root);
|
|
61
65
|
});
|
|
62
66
|
|
|
@@ -130,10 +134,26 @@ describe("site header more dropdown", () => {
|
|
|
130
134
|
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
131
135
|
expect(popover.getAttribute("aria-hidden")).toBe("true");
|
|
132
136
|
});
|
|
137
|
+
|
|
138
|
+
it("removes dropdown listeners when destroyed", () => {
|
|
139
|
+
const trigger = root.querySelector(
|
|
140
|
+
".site-header-more-btn",
|
|
141
|
+
) as HTMLButtonElement;
|
|
142
|
+
const popover = root.querySelector(
|
|
143
|
+
".site-header-more-popover",
|
|
144
|
+
) as HTMLElement;
|
|
145
|
+
|
|
146
|
+
destroySiteHeaderNav(root);
|
|
147
|
+
trigger.click();
|
|
148
|
+
|
|
149
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
150
|
+
expect(popover.getAttribute("aria-hidden")).toBe("true");
|
|
151
|
+
});
|
|
133
152
|
});
|
|
134
153
|
|
|
135
154
|
describe("site header nav drawer", () => {
|
|
136
155
|
let initSiteHeaderNav: SiteHeaderInit;
|
|
156
|
+
let destroySiteHeaderNav: SiteHeaderDestroy;
|
|
137
157
|
let root: HTMLElement;
|
|
138
158
|
|
|
139
159
|
beforeEach(async () => {
|
|
@@ -141,7 +161,8 @@ describe("site header nav drawer", () => {
|
|
|
141
161
|
document.documentElement.classList.remove("drawer-open");
|
|
142
162
|
vi.resetModules();
|
|
143
163
|
root = createDrawerDOM();
|
|
144
|
-
({ initSiteHeaderNav } =
|
|
164
|
+
({ initSiteHeaderNav, destroySiteHeaderNav } =
|
|
165
|
+
await import("../site-header-nav.js"));
|
|
145
166
|
initSiteHeaderNav(root);
|
|
146
167
|
});
|
|
147
168
|
|
|
@@ -247,4 +268,17 @@ describe("site header nav drawer", () => {
|
|
|
247
268
|
expect(drawer.getAttribute("aria-hidden")).toBe("true");
|
|
248
269
|
expect(hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
249
270
|
});
|
|
271
|
+
|
|
272
|
+
it("removes drawer listeners when destroyed", () => {
|
|
273
|
+
const hamburger = root.querySelector(
|
|
274
|
+
".site-header-hamburger",
|
|
275
|
+
) as HTMLButtonElement;
|
|
276
|
+
const drawer = root.querySelector("#site-nav-drawer") as HTMLElement;
|
|
277
|
+
|
|
278
|
+
destroySiteHeaderNav(root);
|
|
279
|
+
hamburger.click();
|
|
280
|
+
|
|
281
|
+
expect(drawer.getAttribute("aria-hidden")).toBe("true");
|
|
282
|
+
expect(hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
283
|
+
});
|
|
250
284
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { JSONContent } from "@tiptap/core";
|
|
3
|
+
|
|
4
|
+
import { promoteLeadingH1Title } from "../compose-title-from-heading.js";
|
|
5
|
+
|
|
6
|
+
function doc(...content: JSONContent[]): JSONContent {
|
|
7
|
+
return { type: "doc", content };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function paragraph(text = ""): JSONContent {
|
|
11
|
+
return text
|
|
12
|
+
? { type: "paragraph", content: [{ type: "text", text }] }
|
|
13
|
+
: { type: "paragraph" };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function heading(level: number, text = ""): JSONContent {
|
|
17
|
+
return text
|
|
18
|
+
? {
|
|
19
|
+
type: "heading",
|
|
20
|
+
attrs: { level },
|
|
21
|
+
content: [{ type: "text", text }],
|
|
22
|
+
}
|
|
23
|
+
: { type: "heading", attrs: { level } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("promoteLeadingH1Title", () => {
|
|
27
|
+
it("extracts the first non-empty H1 and removes it from the body", () => {
|
|
28
|
+
const result = promoteLeadingH1Title(
|
|
29
|
+
doc(paragraph(), heading(1, " My Title "), paragraph("Body")),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result?.title).toBe("My Title");
|
|
33
|
+
expect(result?.headingIndex).toBe(1);
|
|
34
|
+
expect(result?.bodyJson).toEqual(doc(paragraph("Body")));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns null when the first non-empty block is not an H1", () => {
|
|
38
|
+
const result = promoteLeadingH1Title(
|
|
39
|
+
doc(paragraph("Intro"), heading(1, "Title")),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null for empty H1 blocks", () => {
|
|
46
|
+
const result = promoteLeadingH1Title(doc(heading(1), paragraph("Body")));
|
|
47
|
+
|
|
48
|
+
expect(result).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns a null body when the promoted H1 is the only content", () => {
|
|
52
|
+
const result = promoteLeadingH1Title(doc(heading(1, "Only title")));
|
|
53
|
+
|
|
54
|
+
expect(result?.title).toBe("Only title");
|
|
55
|
+
expect(result?.bodyJson).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -7,23 +7,29 @@ import {
|
|
|
7
7
|
} from "../jant-command-palette.js";
|
|
8
8
|
import type { JantCommandPalette } from "../jant-command-palette.js";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
interface MockPaletteItem {
|
|
11
|
+
title: string;
|
|
12
|
+
path: string;
|
|
13
|
+
type: "post" | "collection" | "system";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mockPaletteApi(items: MockPaletteItem[] = []) {
|
|
17
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
18
|
+
async () =>
|
|
19
|
+
new Response(JSON.stringify({ items }), {
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
}),
|
|
15
22
|
);
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
async function renderPalette(query: string) {
|
|
19
|
-
mockPaletteApi();
|
|
25
|
+
async function renderPalette(query: string, items: MockPaletteItem[] = []) {
|
|
26
|
+
mockPaletteApi(items);
|
|
20
27
|
const el = document.createElement(
|
|
21
28
|
"jant-command-palette",
|
|
22
29
|
) as JantCommandPalette;
|
|
23
|
-
el._open = true;
|
|
24
|
-
el._query = query;
|
|
25
30
|
document.body.appendChild(el);
|
|
26
|
-
await
|
|
31
|
+
await el.open();
|
|
32
|
+
el._query = query;
|
|
27
33
|
await el.updateComplete;
|
|
28
34
|
return el;
|
|
29
35
|
}
|
|
@@ -39,6 +45,7 @@ describe("JantCommandPalette slash path mode", () => {
|
|
|
39
45
|
vi.restoreAllMocks();
|
|
40
46
|
document.body.innerHTML = "";
|
|
41
47
|
globalThis.localStorage.clear();
|
|
48
|
+
globalThis.history.replaceState({}, "", "/");
|
|
42
49
|
});
|
|
43
50
|
|
|
44
51
|
it("shows go-to-path first and search second for slash queries", async () => {
|
|
@@ -61,3 +68,123 @@ describe("JantCommandPalette slash path mode", () => {
|
|
|
61
68
|
expect(resultTitles(el)).toEqual(["Go to /notes", 'Search for "notes"']);
|
|
62
69
|
});
|
|
63
70
|
});
|
|
71
|
+
|
|
72
|
+
describe("JantCommandPalette persistent search action", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
document.body.innerHTML = "";
|
|
76
|
+
globalThis.localStorage.clear();
|
|
77
|
+
globalThis.history.replaceState({}, "", "/");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const matchingItems: MockPaletteItem[] = Array.from(
|
|
81
|
+
{ length: 12 },
|
|
82
|
+
(_, index) => ({
|
|
83
|
+
title: `Match ${index + 1}`,
|
|
84
|
+
path: `match-${index + 1}`,
|
|
85
|
+
type: "post",
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
it("keeps search visible below all navigation matches", async () => {
|
|
90
|
+
const el = await renderPalette("match", matchingItems);
|
|
91
|
+
|
|
92
|
+
expect(
|
|
93
|
+
el.querySelectorAll(".command-palette-results > .command-palette-result"),
|
|
94
|
+
).toHaveLength(12);
|
|
95
|
+
expect(resultTitles(el)).toEqual([
|
|
96
|
+
"Match 1",
|
|
97
|
+
"Match 2",
|
|
98
|
+
"Match 3",
|
|
99
|
+
"Match 4",
|
|
100
|
+
"Match 5",
|
|
101
|
+
"Match 6",
|
|
102
|
+
"Match 7",
|
|
103
|
+
"Match 8",
|
|
104
|
+
"Match 9",
|
|
105
|
+
"Match 10",
|
|
106
|
+
"Match 11",
|
|
107
|
+
"Match 12",
|
|
108
|
+
'Search all content for "match"',
|
|
109
|
+
]);
|
|
110
|
+
expect(
|
|
111
|
+
el
|
|
112
|
+
.querySelector(".command-palette-results-container")
|
|
113
|
+
?.lastElementChild?.classList.contains("command-palette-search-action"),
|
|
114
|
+
).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("uses plain Enter without a shortcut hint when search is the only result", async () => {
|
|
118
|
+
const el = await renderPalette("nothing", matchingItems);
|
|
119
|
+
const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
|
|
120
|
+
|
|
121
|
+
expect(resultTitles(el)).toEqual(['Search all content for "nothing"']);
|
|
122
|
+
expect(el.querySelector(".command-palette-result-shortcut")).toBeNull();
|
|
123
|
+
|
|
124
|
+
dialog?.dispatchEvent(
|
|
125
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
126
|
+
key: "Enter",
|
|
127
|
+
bubbles: true,
|
|
128
|
+
cancelable: true,
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(globalThis.location.pathname).toBe("/search");
|
|
133
|
+
expect(globalThis.location.search).toBe("?q=nothing");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("wraps ArrowUp from the best match to the search action", async () => {
|
|
137
|
+
const el = await renderPalette("match", matchingItems);
|
|
138
|
+
const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
|
|
139
|
+
|
|
140
|
+
dialog?.dispatchEvent(
|
|
141
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
142
|
+
key: "ArrowUp",
|
|
143
|
+
bubbles: true,
|
|
144
|
+
cancelable: true,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
await el.updateComplete;
|
|
148
|
+
|
|
149
|
+
expect(
|
|
150
|
+
el.querySelector(
|
|
151
|
+
".command-palette-result-selected .command-palette-result-title",
|
|
152
|
+
)?.textContent,
|
|
153
|
+
).toBe('Search all content for "match"');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it.each(["metaKey", "ctrlKey"] as const)(
|
|
157
|
+
"runs full-text search with the %s Enter shortcut",
|
|
158
|
+
async (modifier) => {
|
|
159
|
+
const el = await renderPalette("match", matchingItems);
|
|
160
|
+
const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
|
|
161
|
+
|
|
162
|
+
dialog?.dispatchEvent(
|
|
163
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
164
|
+
key: "Enter",
|
|
165
|
+
[modifier]: true,
|
|
166
|
+
bubbles: true,
|
|
167
|
+
cancelable: true,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(globalThis.location.pathname).toBe("/search");
|
|
172
|
+
expect(globalThis.location.search).toBe("?q=match");
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
it("keeps plain Enter on the best navigation match", async () => {
|
|
177
|
+
const el = await renderPalette("match", matchingItems);
|
|
178
|
+
const dialog = el.querySelector<HTMLDialogElement>(".command-palette");
|
|
179
|
+
|
|
180
|
+
dialog?.dispatchEvent(
|
|
181
|
+
new globalThis.KeyboardEvent("keydown", {
|
|
182
|
+
key: "Enter",
|
|
183
|
+
bubbles: true,
|
|
184
|
+
cancelable: true,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(globalThis.location.pathname).toBe("/match-1");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -3077,6 +3077,9 @@ describe("JantComposeDialog", () => {
|
|
|
3077
3077
|
expect(css).toMatch(
|
|
3078
3078
|
/\.compose-reply-compose-layout\s+\.compose-thread-post-header\s*\+\s*\.compose-body\s*\{[\s\S]*padding-top:\s*12px;/,
|
|
3079
3079
|
);
|
|
3080
|
+
expect(css).toMatch(
|
|
3081
|
+
/@media \(max-width:\s*760px\),\s*\(hover:\s*none\) and \(pointer:\s*coarse\)\s*\{[\s\S]*\.compose-thread-layout\s*\{[\s\S]*padding-top:\s*0\.75rem;[\s\S]*\.compose-thread-layout::before\s*\{[\s\S]*top:\s*0\.75rem;/,
|
|
3082
|
+
);
|
|
3080
3083
|
expect(css).toMatch(
|
|
3081
3084
|
/\.compose-dialog,[\s\S]*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-quote-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-leading:\s*1\.32;/,
|
|
3082
3085
|
);
|
|
@@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
4
4
|
vi.mock("../../upload-with-metadata.js", () => ({
|
|
5
5
|
uploadWithMetadata: vi.fn(),
|
|
6
6
|
}));
|
|
7
|
-
import type { Editor } from "@tiptap/core";
|
|
7
|
+
import type { Editor, JSONContent } from "@tiptap/core";
|
|
8
8
|
import type { Slice } from "@tiptap/pm/model";
|
|
9
9
|
import * as ProseMirrorView from "@tiptap/pm/view";
|
|
10
10
|
import type { ComposeLabels } from "../compose-types.js";
|
|
@@ -301,6 +301,25 @@ async function createElement(
|
|
|
301
301
|
return el;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
function tiptapDoc(...content: JSONContent[]): JSONContent {
|
|
305
|
+
return { type: "doc", content };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tiptapHeading(level: number, text: string): JSONContent {
|
|
309
|
+
return {
|
|
310
|
+
type: "heading",
|
|
311
|
+
attrs: { level },
|
|
312
|
+
content: [{ type: "text", text }],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function tiptapParagraph(text: string): JSONContent {
|
|
317
|
+
return {
|
|
318
|
+
type: "paragraph",
|
|
319
|
+
content: [{ type: "text", text }],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
async function toggleEmojiPicker(el: JantComposeEditor) {
|
|
305
324
|
(
|
|
306
325
|
el as unknown as {
|
|
@@ -905,6 +924,77 @@ describe("JantComposeEditor", () => {
|
|
|
905
924
|
expect(el.getData().title).toBe("My Title");
|
|
906
925
|
});
|
|
907
926
|
|
|
927
|
+
it("promotes a leading H1 to the note title", async () => {
|
|
928
|
+
const el = await createElement("note");
|
|
929
|
+
const editor = requireEditor(el);
|
|
930
|
+
editor.commands.setContent(
|
|
931
|
+
tiptapDoc(tiptapHeading(1, "My Markdown Title"), tiptapParagraph("Body")),
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
el.promoteLeadingH1Title({ force: true });
|
|
935
|
+
await el.updateComplete;
|
|
936
|
+
|
|
937
|
+
const data = el.getData();
|
|
938
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
939
|
+
|
|
940
|
+
expect(data.title).toBe("My Markdown Title");
|
|
941
|
+
expect(el._showTitle).toBe(true);
|
|
942
|
+
expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("keeps a leading H1 in the body while the cursor is still in it", async () => {
|
|
946
|
+
const el = await createElement("note");
|
|
947
|
+
const editor = requireEditor(el);
|
|
948
|
+
|
|
949
|
+
editor.commands.setContent(tiptapDoc(tiptapHeading(1, "Draft title")));
|
|
950
|
+
await el.updateComplete;
|
|
951
|
+
|
|
952
|
+
const data = el.getData();
|
|
953
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
954
|
+
|
|
955
|
+
expect(data.title).toBe("");
|
|
956
|
+
expect(el._showTitle).toBe(false);
|
|
957
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("does not overwrite an explicit note title with a leading H1", async () => {
|
|
961
|
+
const el = await createElement("note");
|
|
962
|
+
const editor = requireEditor(el);
|
|
963
|
+
el._title = "Manual title";
|
|
964
|
+
el._showTitle = true;
|
|
965
|
+
|
|
966
|
+
editor.commands.setContent(
|
|
967
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
el.promoteLeadingH1Title({ force: true });
|
|
971
|
+
await el.updateComplete;
|
|
972
|
+
|
|
973
|
+
const data = el.getData();
|
|
974
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
975
|
+
|
|
976
|
+
expect(data.title).toBe("Manual title");
|
|
977
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("does not promote a leading H1 for link posts", async () => {
|
|
981
|
+
const el = await createElement("link");
|
|
982
|
+
const editor = requireEditor(el);
|
|
983
|
+
|
|
984
|
+
editor.commands.setContent(
|
|
985
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
el.promoteLeadingH1Title({ force: true });
|
|
989
|
+
await el.updateComplete;
|
|
990
|
+
|
|
991
|
+
const data = el.getData();
|
|
992
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
993
|
+
|
|
994
|
+
expect(data.title).toBe("");
|
|
995
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
996
|
+
});
|
|
997
|
+
|
|
908
998
|
it("omits rating when the rating control is hidden", async () => {
|
|
909
999
|
const el = await createElement("note");
|
|
910
1000
|
el._rating = 3;
|
|
@@ -1016,7 +1106,9 @@ describe("JantComposeEditor", () => {
|
|
|
1016
1106
|
image.dispatchEvent(new Event("error"));
|
|
1017
1107
|
await el.updateComplete;
|
|
1018
1108
|
|
|
1019
|
-
|
|
1109
|
+
const fallback = el.querySelector("[data-preview-failed='image']");
|
|
1110
|
+
expect(fallback).not.toBeNull();
|
|
1111
|
+
expect(fallback?.textContent).toContain("Image unavailable");
|
|
1020
1112
|
expect(el.querySelector(".compose-attachment-remove")).not.toBeNull();
|
|
1021
1113
|
expect(el.querySelector(".compose-attachment-img")).toBeNull();
|
|
1022
1114
|
});
|