@jant/core 0.6.13 → 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.
Files changed (30) hide show
  1. package/dist/{app-HnzmsaqU.js → app-2i7-WQHg.js} +26 -22
  2. package/dist/app-DZDlmh7C.js +6 -0
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/{client-3_OaxhTs.js → client-BvID5ucz.js} +1 -1
  5. package/dist/client/_assets/client-Cevfpm8H.css +2 -0
  6. package/dist/client/_assets/{client-auth-TsYq90xm.js → client-auth-v3yvhbgZ.js} +175 -172
  7. package/dist/{export-Dfbq9aot.js → export-DWn149b7.js} +14 -6
  8. package/dist/{github-sync-BGTLx8Mf.js → github-sync-C0zyF6XS.js} +2 -2
  9. package/dist/{github-sync--PSoE-Ne.js → github-sync-Ckk0sJxK.js} +1 -1
  10. package/dist/index.js +3 -3
  11. package/dist/node.js +4 -4
  12. package/package.json +1 -1
  13. package/src/client/components/__tests__/jant-command-palette.test.ts +137 -10
  14. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
  15. package/src/client/components/jant-command-palette.ts +100 -70
  16. package/src/client/components/jant-compose-dialog.ts +1 -1
  17. package/src/client/tiptap/__tests__/footnotes.test.ts +284 -0
  18. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
  19. package/src/client/tiptap/bubble-menu.ts +3 -3
  20. package/src/client/tiptap/footnotes.ts +156 -0
  21. package/src/client/tiptap/markdown-clipboard.ts +7 -2
  22. package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
  23. package/src/lib/markdown-manager.ts +14 -5
  24. package/src/styles/ui.css +116 -28
  25. package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
  26. package/src/ui/layouts/BaseLayout.tsx +1 -1
  27. package/src/ui/layouts/SiteLayout.tsx +19 -13
  28. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
  29. package/dist/app-CWJFPjuR.js +0 -6
  30. package/dist/client/_assets/client-DCrcHVJl.css +0 -2
@@ -2994,7 +2994,8 @@ function createMarkdownContentExtensions(options = {}) {
2994
2994
  openOnClick: false,
2995
2995
  autolink: false
2996
2996
  },
2997
- codeBlock: false
2997
+ codeBlock: false,
2998
+ trailingNode: { notAfter: ["footnoteDefinition"] }
2998
2999
  }),
2999
3000
  MarkdownCodeBlock,
3000
3001
  Table.configure({
@@ -3013,9 +3014,16 @@ function createMarkdownContentExtensions(options = {}) {
3013
3014
  MarkdownFootnoteDefinition
3014
3015
  ];
3015
3016
  }
3016
- function normalizeMarkdownDoc(node) {
3017
+ /**
3018
+ * Normalizes Markdown parser output before it enters the editor schema.
3019
+ *
3020
+ * @param node - Parsed Tiptap document or descendant node
3021
+ * @returns A normalized copy safe to load into an editor
3022
+ * @example
3023
+ * const normalized = normalizeMarkdownDocument(markdownManager.parse(source));
3024
+ */ function normalizeMarkdownDocument(node) {
3017
3025
  const normalized = { ...node };
3018
- if (normalized.content) normalized.content = normalized.content.map(normalizeMarkdownDoc);
3026
+ if (normalized.content) normalized.content = normalized.content.map(normalizeMarkdownDocument);
3019
3027
  if (normalized.marks) normalized.marks = normalized.marks.map((mark) => {
3020
3028
  if (!mark || typeof mark !== "object") return mark;
3021
3029
  const nextMark = {
@@ -3040,8 +3048,8 @@ function normalizeMarkdownDoc(node) {
3040
3048
  for (let index = 0; index < normalized.content.length; index += 1) {
3041
3049
  const child = normalized.content[index];
3042
3050
  const nextChild = normalized.content[index + 1];
3043
- if (child?.type === "text" && typeof child.text === "string" && nextChild?.type === "footnoteReference" && child.text.endsWith("\n")) {
3044
- const trimmedText = child.text.replace(/\n$/, "");
3051
+ if (child?.type === "text" && typeof child.text === "string" && nextChild?.type === "footnoteReference" && /\n[ \t]*$/.test(child.text)) {
3052
+ const trimmedText = child.text.replace(/\n[ \t]*$/, "");
3045
3053
  if (trimmedText) nextContent.push({
3046
3054
  ...child,
3047
3055
  text: trimmedText
@@ -3069,7 +3077,7 @@ function getMarkdownManager() {
3069
3077
  return sharedMarkdownManager;
3070
3078
  }
3071
3079
  function parseMarkdownDocument(markdown) {
3072
- return normalizeMarkdownDoc(getMarkdownManager().parse(markdown));
3080
+ return normalizeMarkdownDocument(getMarkdownManager().parse(markdown));
3073
3081
  }
3074
3082
  function serializeMarkdownDocument(doc) {
3075
3083
  return expandCodeBlockFences(getMarkdownManager().serialize(normalizeFootnoteArtifacts(doc)));
@@ -1,4 +1,4 @@
1
1
  import "./url-BMYO-Zlt.js";
2
- import "./export-Dfbq9aot.js";
3
- import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync--PSoE-Ne.js";
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-Dfbq9aot.js";
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-HnzmsaqU.js";
3
- import { S as time_exports, a as markdown_exports } from "./export-Dfbq9aot.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-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--PSoE-Ne.js";
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-HnzmsaqU.js";
3
- import { t as createExportService } from "./export-Dfbq9aot.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-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--PSoE-Ne.js";
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-CWJFPjuR.js")).createApp()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
- function mockPaletteApi() {
11
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
12
- new Response(JSON.stringify({ items: [] }), {
13
- headers: { "Content-Type": "application/json" },
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 Promise.resolve();
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
  );
@@ -30,6 +30,19 @@ interface CommandItem {
30
30
  action: () => void;
31
31
  }
32
32
 
33
+ interface DisplayItem {
34
+ label: string;
35
+ secondary?: string;
36
+ icon: string;
37
+ /** For search-mode items: the query to execute */
38
+ searchQuery?: string;
39
+ /** For path-mode items: the local path to open */
40
+ pathTarget?: string;
41
+ /** Keep cross-mode actions visible below the scrollable result list. */
42
+ placement?: "results" | "footer";
43
+ shortcut?: string;
44
+ }
45
+
33
46
  // ---------------------------------------------------------------------------
34
47
  // SVG icon paths (reuse existing Lucide-style icons from the codebase)
35
48
  // ---------------------------------------------------------------------------
@@ -280,15 +293,7 @@ export class JantCommandPalette extends LitElement {
280
293
  return COMMANDS.filter((c) => !q || c.label.toLowerCase().includes(q));
281
294
  }
282
295
 
283
- get #displayItems(): Array<{
284
- label: string;
285
- secondary?: string;
286
- icon: string;
287
- /** For search-mode items: the query to execute */
288
- searchQuery?: string;
289
- /** For path-mode items: the local path to open */
290
- pathTarget?: string;
291
- }> {
296
+ get #displayItems(): DisplayItem[] {
292
297
  const mode = this.#mode;
293
298
 
294
299
  if (mode === "command") {
@@ -320,13 +325,7 @@ export class JantCommandPalette extends LitElement {
320
325
  if (mode === "path") {
321
326
  const target = getPathCommandTarget(this._query);
322
327
  const searchQuery = getPathCommandSearchQuery(this._query);
323
- const items: Array<{
324
- label: string;
325
- secondary?: string;
326
- icon: string;
327
- searchQuery?: string;
328
- pathTarget?: string;
329
- }> = [
328
+ const items: DisplayItem[] = [
330
329
  {
331
330
  label: `Go to ${target}`,
332
331
  icon: ICONS.path,
@@ -346,25 +345,25 @@ export class JantCommandPalette extends LitElement {
346
345
  }
347
346
 
348
347
  // Navigate mode — show all items when no query (autocomplete)
349
- const navItems: Array<{
350
- label: string;
351
- secondary?: string;
352
- icon: string;
353
- searchQuery?: string;
354
- pathTarget?: string;
355
- }> = this.#navigateItems.map((item) => ({
348
+ // Rank direct destinations and let the scrollable result list handle
349
+ // overflow without hiding valid matches.
350
+ const q = normalizeSearch(this._query);
351
+ const navItems: DisplayItem[] = this.#navigateItems.map((item) => ({
356
352
  label: item.title,
357
- secondary: item.type === "system" ? item.path : item.path,
353
+ secondary: item.path,
358
354
  icon: ICONS[item.type],
359
355
  }));
360
356
 
361
- // When navigate mode has no matches, offer a full-text search fallback
362
- const q = normalizeSearch(this._query);
363
- if (navItems.length === 0 && q) {
357
+ // Full-text search is a persistent action, not another navigation result.
358
+ // Keep it outside the scrollable list while retaining it as the final
359
+ // keyboard option.
360
+ if (q) {
364
361
  navItems.push({
365
- label: `Search for "${this._query.trim()}"`,
362
+ label: `Search all content for "${this._query.trim()}"`,
366
363
  icon: ICONS.search,
367
364
  searchQuery: this._query.trim(),
365
+ placement: "footer",
366
+ ...(navItems.length > 0 ? { shortcut: "⌘/Ctrl ↵" } : {}),
368
367
  });
369
368
  }
370
369
 
@@ -375,6 +374,12 @@ export class JantCommandPalette extends LitElement {
375
374
  // Actions
376
375
  // -----------------------------------------------------------------------
377
376
 
377
+ #executeSearch(query: string) {
378
+ saveHistory(SEARCH_HISTORY_KEY, query);
379
+ this.close();
380
+ window.location.href = `/search?q=${encodeURIComponent(query)}`;
381
+ }
382
+
378
383
  #executeItem(index: number) {
379
384
  const mode = this.#mode;
380
385
 
@@ -391,9 +396,7 @@ export class JantCommandPalette extends LitElement {
391
396
  const displayItem = this.#displayItems[index];
392
397
  const q = displayItem?.searchQuery;
393
398
  if (q) {
394
- saveHistory(SEARCH_HISTORY_KEY, q);
395
- this.close();
396
- window.location.href = `/search?q=${encodeURIComponent(q)}`;
399
+ this.#executeSearch(q);
397
400
  }
398
401
  return;
399
402
  }
@@ -408,10 +411,7 @@ export class JantCommandPalette extends LitElement {
408
411
  }
409
412
 
410
413
  if (displayItem?.searchQuery) {
411
- const q = displayItem.searchQuery;
412
- saveHistory(SEARCH_HISTORY_KEY, q);
413
- this.close();
414
- window.location.href = `/search?q=${encodeURIComponent(q)}`;
414
+ this.#executeSearch(displayItem.searchQuery);
415
415
  }
416
416
  return;
417
417
  }
@@ -419,10 +419,7 @@ export class JantCommandPalette extends LitElement {
419
419
  // Navigate mode — check if the selected item is a search fallback
420
420
  const displayItem = this.#displayItems[index];
421
421
  if (displayItem?.searchQuery) {
422
- const q = displayItem.searchQuery;
423
- saveHistory(SEARCH_HISTORY_KEY, q);
424
- this.close();
425
- window.location.href = `/search?q=${encodeURIComponent(q)}`;
422
+ this.#executeSearch(displayItem.searchQuery);
426
423
  return;
427
424
  }
428
425
 
@@ -475,6 +472,19 @@ export class JantCommandPalette extends LitElement {
475
472
  return;
476
473
  }
477
474
 
475
+ if (
476
+ event.key === "Enter" &&
477
+ (event.metaKey || event.ctrlKey) &&
478
+ this.#mode === "navigate"
479
+ ) {
480
+ const query = this._query.trim();
481
+ if (query) {
482
+ event.preventDefault();
483
+ this.#executeSearch(query);
484
+ return;
485
+ }
486
+ }
487
+
478
488
  if (event.key === "Enter") {
479
489
  event.preventDefault();
480
490
  this.#executeItem(this._selectedIndex);
@@ -521,10 +531,49 @@ export class JantCommandPalette extends LitElement {
521
531
  // Render
522
532
  // -----------------------------------------------------------------------
523
533
 
534
+ #renderItem(item: DisplayItem, index: number, footer = false) {
535
+ return html`
536
+ <div
537
+ id="command-palette-item-${index}"
538
+ class=${classMap({
539
+ "command-palette-result": true,
540
+ "command-palette-result-selected": index === this._selectedIndex,
541
+ "command-palette-search-action": footer,
542
+ })}
543
+ role="option"
544
+ aria-selected=${index === this._selectedIndex}
545
+ @click=${this.#handleItemClick(index)}
546
+ >
547
+ <span class="command-palette-result-icon">${unsafeSVG(item.icon)}</span>
548
+ <span class="command-palette-result-body">
549
+ <span class="command-palette-result-title">${item.label}</span>
550
+ ${item.secondary
551
+ ? html`<span class="command-palette-result-path"
552
+ >${item.secondary}</span
553
+ >`
554
+ : nothing}
555
+ </span>
556
+ ${item.shortcut
557
+ ? html`<kbd
558
+ class="command-palette-result-shortcut"
559
+ aria-label="Command or Control plus Enter"
560
+ >${item.shortcut}</kbd
561
+ >`
562
+ : nothing}
563
+ </div>
564
+ `;
565
+ }
566
+
524
567
  render() {
525
568
  if (!this._open) return nothing;
526
569
 
527
570
  const items = this.#displayItems;
571
+ const resultItems = items
572
+ .map((item, index) => ({ item, index }))
573
+ .filter(({ item }) => item.placement !== "footer");
574
+ const footerItems = items
575
+ .map((item, index) => ({ item, index }))
576
+ .filter(({ item }) => item.placement === "footer");
528
577
 
529
578
  return html`
530
579
  <dialog
@@ -563,41 +612,22 @@ export class JantCommandPalette extends LitElement {
563
612
 
564
613
  ${items.length > 0
565
614
  ? html`
566
- <ul
615
+ <div
567
616
  id="command-palette-results"
568
- class="command-palette-results"
617
+ class="command-palette-results-container"
569
618
  role="listbox"
570
619
  >
571
- ${items.map(
572
- (item, i) => html`
573
- <li
574
- id="command-palette-item-${i}"
575
- class=${classMap({
576
- "command-palette-result": true,
577
- "command-palette-result-selected":
578
- i === this._selectedIndex,
579
- })}
580
- role="option"
581
- aria-selected=${i === this._selectedIndex}
582
- @click=${this.#handleItemClick(i)}
583
- >
584
- <span class="command-palette-result-icon"
585
- >${unsafeSVG(item.icon)}</span
586
- >
587
- <span class="command-palette-result-body">
588
- <span class="command-palette-result-title"
589
- >${item.label}</span
590
- >
591
- ${item.secondary
592
- ? html`<span class="command-palette-result-path"
593
- >${item.secondary}</span
594
- >`
595
- : nothing}
596
- </span>
597
- </li>
598
- `,
620
+ ${resultItems.length > 0
621
+ ? html`<div class="command-palette-results">
622
+ ${resultItems.map(({ item, index }) =>
623
+ this.#renderItem(item, index),
624
+ )}
625
+ </div>`
626
+ : nothing}
627
+ ${footerItems.map(({ item, index }) =>
628
+ this.#renderItem(item, index, true),
599
629
  )}
600
- </ul>
630
+ </div>
601
631
  `
602
632
  : this._query.trim() && !this._loading
603
633
  ? html`<div class="command-palette-empty">No results</div>`
@@ -4033,7 +4033,7 @@ export class JantComposeDialog extends LitElement {
4033
4033
  <span class="compose-alt-title">${this.labels.attachedText}</span>
4034
4034
  <button
4035
4035
  type="button"
4036
- class="compose-post-btn ml-auto"
4036
+ class="compose-attached-done"
4037
4037
  @click=${() => this._doneAttachedPanel()}
4038
4038
  >
4039
4039
  ${this.labels.done}