@jant/core 0.5.1 → 0.5.2
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/{app-D_ke5q_u.js → app-CPVwpmb3.js} +35 -15
- package/dist/{app-BpyntjN7.js → app-Z83YzveI.js} +1 -1
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-DkbGkmgp.js → client-8pp1btGZ.js} +1 -1
- package/dist/client/_assets/{client-Hxj-LpVt.css → client-CFegXQty.css} +1 -1
- package/dist/client/_assets/{client-auth-BRrao4p4.js → client-auth-Ds3SVvGh.js} +79 -62
- package/dist/index.js +1 -1
- package/dist/node.js +2 -2
- package/package.json +1 -1
- package/src/client/__tests__/slash-discovery.test.ts +150 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +1 -0
- package/src/client/components/compose-types.ts +1 -0
- package/src/client/components/jant-compose-dialog.ts +8 -0
- package/src/client/components/jant-compose-editor.ts +51 -7
- package/src/client/slash-discovery-bridge.ts +9 -0
- package/src/client/slash-discovery.ts +200 -0
- package/src/client/tiptap/slash-commands.ts +5 -0
- package/src/client-auth.ts +1 -0
- package/src/i18n/locales/public/en.po +5 -0
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +5 -0
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +5 -0
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/render.tsx +3 -0
- package/src/routes/api/__tests__/settings.test.ts +59 -0
- package/src/routes/api/settings.ts +19 -0
- package/src/routes/pages/new.tsx +6 -0
- package/src/styles/ui.css +40 -0
- package/src/types/config.ts +5 -0
- package/src/types/views.ts +1 -0
- package/src/ui/compose/ComposeDialog.tsx +12 -0
- package/src/ui/layouts/SiteLayout.tsx +2 -0
- package/src/ui/pages/ComposePage.tsx +3 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as url_exports } from "./url-umUptr5z.js";
|
|
2
|
-
import { A as NAV_ITEM_TYPES, C as toPostView, D as MAX_MEDIA_ATTACHMENTS, E as FORMATS, M as STATUSES, N as TEXT_ATTACHMENT_CONTENT_FORMATS, O as MAX_PINNED_POSTS, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, f as defaultFeedRenderer, j as SORT_ORDERS, k as MEDIA_KINDS, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-
|
|
2
|
+
import { A as NAV_ITEM_TYPES, C as toPostView, D as MAX_MEDIA_ATTACHMENTS, E as FORMATS, M as STATUSES, N as TEXT_ATTACHMENT_CONTENT_FORMATS, O as MAX_PINNED_POSTS, S as toNavItemViews, T as toSearchResultView, _ as createMediaContext, b as toMediaView, f as defaultFeedRenderer, j as SORT_ORDERS, k as MEDIA_KINDS, t as createApp, v as toArchiveGroups, w as toPostViews, x as toNavItemView, y as toArchiveGroupsWithMedia } from "./app-CPVwpmb3.js";
|
|
3
3
|
import { T as time_exports, a as markdown_exports } from "./export-I9XFTWyO.js";
|
|
4
4
|
import "./env-CgaH9Mut.js";
|
|
5
5
|
import "./github-sync-DBAwA3H9.js";
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./url-umUptr5z.js";
|
|
2
|
-
import { F as BUILTIN_COLOR_THEMES, I as getPublicAssetBasePath, L as isAssetPath, P as buildThemeStyle, a as resolveDatabaseDialect, c as resolveConfig, d as getFontThemeCssVariables, g as schema_exports, h as createNodeDatabase, i as createSiteService, l as BUILTIN_FONT_THEMES, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as pgSchemaBundle, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as getCjkSerifCssVariables } from "./app-
|
|
2
|
+
import { F as BUILTIN_COLOR_THEMES, I as getPublicAssetBasePath, L as isAssetPath, P as buildThemeStyle, a as resolveDatabaseDialect, c as resolveConfig, d as getFontThemeCssVariables, g as schema_exports, h as createNodeDatabase, i as createSiteService, l as BUILTIN_FONT_THEMES, m as sqliteSchemaBundle, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as pgSchemaBundle, r as createNodeRequestRuntime, s as createStorageDriver, t as createApp, u as getCjkSerifCssVariables } from "./app-CPVwpmb3.js";
|
|
3
3
|
import { t as createExportService } from "./export-I9XFTWyO.js";
|
|
4
4
|
import { b as getSiteResolutionMode, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as shouldTrustProxy, y as getPort } from "./env-CgaH9Mut.js";
|
|
5
5
|
import "./github-sync-DBAwA3H9.js";
|
|
@@ -474,7 +474,7 @@ async function createNodeRequestHandler(options) {
|
|
|
474
474
|
async function start(env = process.env, app) {
|
|
475
475
|
const handler = await createNodeRequestHandler({
|
|
476
476
|
env,
|
|
477
|
-
app: async () => app ?? (await import("./app-
|
|
477
|
+
app: async () => app ?? (await import("./app-Z83YzveI.js")).createApp()
|
|
478
478
|
});
|
|
479
479
|
const hostname = resolveHost(env);
|
|
480
480
|
const port = resolvePort(env);
|
package/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
__testOnly,
|
|
6
|
+
hideSlashCommandHint,
|
|
7
|
+
markSlashCommandDiscovered,
|
|
8
|
+
scheduleSlashCommandHint,
|
|
9
|
+
} from "../slash-discovery.js";
|
|
10
|
+
|
|
11
|
+
function createEditorHost(discovered = false): HTMLElement {
|
|
12
|
+
document.body.innerHTML = `
|
|
13
|
+
<jant-compose-editor data-slash-command-discovered="${discovered ? "true" : "false"}">
|
|
14
|
+
<div class="compose-tiptap-body"></div>
|
|
15
|
+
<span class="compose-slash-discovery-hint" aria-hidden="true">Type / for commands</span>
|
|
16
|
+
</jant-compose-editor>
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
return document.querySelector<HTMLElement>(
|
|
20
|
+
"jant-compose-editor",
|
|
21
|
+
) as HTMLElement;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("slash discovery", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
vi.useFakeTimers();
|
|
28
|
+
globalThis.localStorage.clear();
|
|
29
|
+
__testOnly.reset();
|
|
30
|
+
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: true }));
|
|
31
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.useRealTimers();
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("shows the hint after the delay and records one exposure per page", () => {
|
|
40
|
+
const host = createEditorHost();
|
|
41
|
+
|
|
42
|
+
scheduleSlashCommandHint(host);
|
|
43
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
44
|
+
|
|
45
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
expect(__testOnly.readState()).toMatchObject({
|
|
49
|
+
shownCount: 1,
|
|
50
|
+
completed: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// No auto-fade — the hint stays visible until something explicitly hides it.
|
|
54
|
+
vi.advanceTimersByTime(5000);
|
|
55
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
hideSlashCommandHint(host);
|
|
60
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
61
|
+
false,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Refocusing during the same page load shows the hint again
|
|
65
|
+
// without bumping the persisted shownCount.
|
|
66
|
+
scheduleSlashCommandHint(host);
|
|
67
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
68
|
+
|
|
69
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
expect(__testOnly.readState().shownCount).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("hides the hint immediately when called", () => {
|
|
76
|
+
const host = createEditorHost();
|
|
77
|
+
|
|
78
|
+
scheduleSlashCommandHint(host);
|
|
79
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
80
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
hideSlashCommandHint(host);
|
|
85
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
86
|
+
false,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("does not show the hint after the local max has been reached", () => {
|
|
91
|
+
const host = createEditorHost();
|
|
92
|
+
globalThis.localStorage.setItem(
|
|
93
|
+
__testOnly.SLASH_DISCOVERY_STORAGE_KEY,
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
shownCount: __testOnly.SLASH_HINT_MAX_SHOW_COUNT,
|
|
96
|
+
completed: false,
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
scheduleSlashCommandHint(host);
|
|
101
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
102
|
+
|
|
103
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
104
|
+
false,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not show the hint when the DB flag is already set", () => {
|
|
109
|
+
const host = createEditorHost(true);
|
|
110
|
+
|
|
111
|
+
scheduleSlashCommandHint(host);
|
|
112
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
113
|
+
|
|
114
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
115
|
+
false,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not show the hint on mobile widths", () => {
|
|
120
|
+
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: false }));
|
|
121
|
+
const host = createEditorHost();
|
|
122
|
+
|
|
123
|
+
scheduleSlashCommandHint(host);
|
|
124
|
+
vi.advanceTimersByTime(__testOnly.SLASH_HINT_DELAY_MS);
|
|
125
|
+
|
|
126
|
+
expect(host.classList.contains(__testOnly.SLASH_HINT_VISIBLE_CLASS)).toBe(
|
|
127
|
+
false,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("marks the slash command as completed locally and syncs to the server", async () => {
|
|
132
|
+
const host = createEditorHost();
|
|
133
|
+
|
|
134
|
+
markSlashCommandDiscovered();
|
|
135
|
+
await Promise.resolve();
|
|
136
|
+
|
|
137
|
+
expect(__testOnly.readState()).toMatchObject({
|
|
138
|
+
shownCount: __testOnly.SLASH_HINT_MAX_SHOW_COUNT,
|
|
139
|
+
completed: true,
|
|
140
|
+
});
|
|
141
|
+
expect(host.dataset.slashCommandDiscovered).toBe("true");
|
|
142
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
143
|
+
__testOnly.SLASH_DISCOVERY_API_PATH,
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
method: "POST",
|
|
146
|
+
credentials: "same-origin",
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -534,6 +534,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
534
534
|
pageMode: { type: Boolean, attribute: "page-mode" },
|
|
535
535
|
closeHref: { type: String, attribute: "close-href" },
|
|
536
536
|
autoRestoreDraft: { type: Boolean, attribute: "auto-restore-draft" },
|
|
537
|
+
slashCommandDiscovered: {
|
|
538
|
+
type: Boolean,
|
|
539
|
+
attribute: "slash-command-discovered",
|
|
540
|
+
},
|
|
537
541
|
_format: { state: true },
|
|
538
542
|
_status: { state: true },
|
|
539
543
|
_loading: { state: true },
|
|
@@ -578,6 +582,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
578
582
|
declare pageMode: boolean;
|
|
579
583
|
declare closeHref: string;
|
|
580
584
|
declare autoRestoreDraft: boolean;
|
|
585
|
+
declare slashCommandDiscovered: boolean;
|
|
581
586
|
declare _format: ComposeFormat;
|
|
582
587
|
declare _status: "published" | "draft";
|
|
583
588
|
declare _loading: boolean;
|
|
@@ -665,6 +670,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
665
670
|
this.pageMode = false;
|
|
666
671
|
this.closeHref = "/";
|
|
667
672
|
this.autoRestoreDraft = false;
|
|
673
|
+
this.slashCommandDiscovered = false;
|
|
668
674
|
this._format = "note";
|
|
669
675
|
this._status = "published";
|
|
670
676
|
this._loading = false;
|
|
@@ -5283,6 +5289,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5283
5289
|
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
5284
5290
|
.threadItem=${true}
|
|
5285
5291
|
.removable=${showRemove}
|
|
5292
|
+
.slashCommandDiscovered=${this.slashCommandDiscovered}
|
|
5286
5293
|
data-thread-id=${item.id}
|
|
5287
5294
|
></jant-compose-editor>
|
|
5288
5295
|
</div>
|
|
@@ -5387,6 +5394,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5387
5394
|
.format=${this._format}
|
|
5388
5395
|
.labels=${this.labels}
|
|
5389
5396
|
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
5397
|
+
.slashCommandDiscovered=${this.slashCommandDiscovered}
|
|
5390
5398
|
></jant-compose-editor>`;
|
|
5391
5399
|
|
|
5392
5400
|
return html`
|
|
@@ -48,6 +48,11 @@ import {
|
|
|
48
48
|
} from "../tiptap/inline-image-upload.js";
|
|
49
49
|
import { isSafeAbsoluteUrl } from "../../lib/url.js";
|
|
50
50
|
import { randomUUID } from "../random-uuid.js";
|
|
51
|
+
import {
|
|
52
|
+
hideSlashCommandHint,
|
|
53
|
+
markSlashCommandDiscovered,
|
|
54
|
+
scheduleSlashCommandHint,
|
|
55
|
+
} from "../slash-discovery.js";
|
|
51
56
|
|
|
52
57
|
interface ComposeFilePickerCloseDetail {
|
|
53
58
|
cancelled: boolean;
|
|
@@ -172,6 +177,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
172
177
|
uploadMaxFileSize: { type: Number },
|
|
173
178
|
threadItem: { type: Boolean, attribute: "thread-item" },
|
|
174
179
|
removable: { type: Boolean },
|
|
180
|
+
slashCommandDiscovered: { type: Boolean },
|
|
175
181
|
_title: { state: true },
|
|
176
182
|
_bodyJson: { state: true },
|
|
177
183
|
_url: { state: true },
|
|
@@ -196,6 +202,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
196
202
|
declare uploadMaxFileSize: number;
|
|
197
203
|
declare threadItem: boolean;
|
|
198
204
|
declare removable: boolean;
|
|
205
|
+
declare slashCommandDiscovered: boolean;
|
|
199
206
|
declare _title: string;
|
|
200
207
|
declare _bodyJson: JSONContent | null;
|
|
201
208
|
declare _url: string;
|
|
@@ -242,6 +249,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
242
249
|
this.uploadMaxFileSize = 500;
|
|
243
250
|
this.threadItem = false;
|
|
244
251
|
this.removable = false;
|
|
252
|
+
this.slashCommandDiscovered = false;
|
|
245
253
|
this._title = "";
|
|
246
254
|
this._bodyJson = null;
|
|
247
255
|
this._url = "";
|
|
@@ -264,6 +272,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
264
272
|
connectedCallback() {
|
|
265
273
|
super.connectedCallback();
|
|
266
274
|
document.addEventListener("jant:slash-image", this._onSlashImage);
|
|
275
|
+
document.addEventListener(
|
|
276
|
+
"jant:slash-command-discovered",
|
|
277
|
+
this._onSlashCommandDiscovered,
|
|
278
|
+
);
|
|
267
279
|
}
|
|
268
280
|
|
|
269
281
|
disconnectedCallback() {
|
|
@@ -274,13 +286,22 @@ export class JantComposeEditor extends LitElement {
|
|
|
274
286
|
this.#sortable?.destroy();
|
|
275
287
|
this.#sortable = null;
|
|
276
288
|
document.removeEventListener("jant:slash-image", this._onSlashImage);
|
|
289
|
+
document.removeEventListener(
|
|
290
|
+
"jant:slash-command-discovered",
|
|
291
|
+
this._onSlashCommandDiscovered,
|
|
292
|
+
);
|
|
277
293
|
document.removeEventListener("click", this._onDocClickBound);
|
|
294
|
+
hideSlashCommandHint(this);
|
|
278
295
|
this._emojiContainer?.remove();
|
|
279
296
|
this._emojiPickerEl = null;
|
|
280
297
|
this._filePickerCleanup?.();
|
|
281
298
|
this._filePickerCleanup = null;
|
|
282
299
|
}
|
|
283
300
|
|
|
301
|
+
private _onSlashCommandDiscovered = () => {
|
|
302
|
+
markSlashCommandDiscovered();
|
|
303
|
+
};
|
|
304
|
+
|
|
284
305
|
private _onSlashImage = () => {
|
|
285
306
|
// Skip when fullscreen is open — it has its own handler
|
|
286
307
|
if (document.querySelector(".compose-fullscreen-dialog[open]")) return;
|
|
@@ -686,6 +707,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
686
707
|
const container = this.querySelector<HTMLElement>(".compose-tiptap-body");
|
|
687
708
|
if (!container || this._editor) return;
|
|
688
709
|
|
|
710
|
+
this.dataset.slashCommandDiscovered = this.slashCommandDiscovered
|
|
711
|
+
? "true"
|
|
712
|
+
: "false";
|
|
713
|
+
|
|
689
714
|
this._editor = createTiptapEditor({
|
|
690
715
|
element: container,
|
|
691
716
|
placeholder:
|
|
@@ -697,9 +722,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
697
722
|
onUpdate: (json) => {
|
|
698
723
|
this._bodyJson = json;
|
|
699
724
|
this._ensureScrollBuffer();
|
|
725
|
+
hideSlashCommandHint(this);
|
|
700
726
|
},
|
|
701
727
|
onFocus: () => {
|
|
702
728
|
this._lastFocusedField = null;
|
|
729
|
+
if (this._editor?.isEmpty) {
|
|
730
|
+
scheduleSlashCommandHint(this);
|
|
731
|
+
}
|
|
703
732
|
},
|
|
704
733
|
onSelectionUpdate: (selection) => {
|
|
705
734
|
this._lastEditorSelection = selection;
|
|
@@ -1655,7 +1684,12 @@ export class JantComposeEditor extends LitElement {
|
|
|
1655
1684
|
</div>
|
|
1656
1685
|
`
|
|
1657
1686
|
: nothing}
|
|
1658
|
-
<div class="compose-tiptap-
|
|
1687
|
+
<div class="compose-tiptap-wrap">
|
|
1688
|
+
<div class="compose-tiptap-body"></div>
|
|
1689
|
+
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
1690
|
+
${this.labels.slashHint}
|
|
1691
|
+
</span>
|
|
1692
|
+
</div>
|
|
1659
1693
|
</div>
|
|
1660
1694
|
`;
|
|
1661
1695
|
}
|
|
@@ -1723,9 +1757,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
1723
1757
|
</p>`
|
|
1724
1758
|
: nothing}
|
|
1725
1759
|
<div class="compose-divider"></div>
|
|
1726
|
-
<div
|
|
1727
|
-
|
|
1728
|
-
|
|
1760
|
+
<div class="compose-tiptap-wrap">
|
|
1761
|
+
<div
|
|
1762
|
+
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-link"
|
|
1763
|
+
></div>
|
|
1764
|
+
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
1765
|
+
${this.labels.slashHint}
|
|
1766
|
+
</span>
|
|
1767
|
+
</div>
|
|
1729
1768
|
</div>
|
|
1730
1769
|
`;
|
|
1731
1770
|
}
|
|
@@ -1789,9 +1828,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
1789
1828
|
class="compose-divider compose-divider-quote"
|
|
1790
1829
|
aria-hidden="true"
|
|
1791
1830
|
></div>
|
|
1792
|
-
<div
|
|
1793
|
-
|
|
1794
|
-
|
|
1831
|
+
<div class="compose-tiptap-wrap">
|
|
1832
|
+
<div
|
|
1833
|
+
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-thoughts-quote"
|
|
1834
|
+
></div>
|
|
1835
|
+
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
1836
|
+
${this.labels.slashHint}
|
|
1837
|
+
</span>
|
|
1838
|
+
</div>
|
|
1795
1839
|
</div>
|
|
1796
1840
|
`;
|
|
1797
1841
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash Command Discovery
|
|
3
|
+
*
|
|
4
|
+
* Surfaces a small "Type / for commands" hint over the TipTap compose editor on
|
|
5
|
+
* first focus. Mirrors the compose-open-shortcut pattern:
|
|
6
|
+
* - localStorage tracks shownCount + completed for fast read on subsequent visits
|
|
7
|
+
* - DB (synced once via POST) is the durable cross-device "user has used this" flag
|
|
8
|
+
* - The DB flag is hydrated server-side onto the editor host as
|
|
9
|
+
* `data-slash-command-discovered="true"`
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SLASH_DISCOVERY_STORAGE_KEY = "jant.slashCommandDiscovery";
|
|
13
|
+
const SLASH_DISCOVERY_API_PATH = "/api/settings/discovery/slash-command";
|
|
14
|
+
const SLASH_HINT_DELAY_MS = 600;
|
|
15
|
+
const SLASH_HINT_MAX_SHOW_COUNT = 3;
|
|
16
|
+
const SLASH_HINT_VISIBLE_CLASS = "compose-slash-discovery-visible";
|
|
17
|
+
|
|
18
|
+
interface SlashDiscoveryState {
|
|
19
|
+
shownCount: number;
|
|
20
|
+
completed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let exposureRecordedThisSession = false;
|
|
24
|
+
let showTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
let activeHost: HTMLElement | null = null;
|
|
26
|
+
|
|
27
|
+
function canUseLocalStorage(): boolean {
|
|
28
|
+
try {
|
|
29
|
+
return typeof globalThis.localStorage !== "undefined";
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readState(): SlashDiscoveryState {
|
|
36
|
+
if (!canUseLocalStorage()) {
|
|
37
|
+
return { shownCount: 0, completed: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const raw = globalThis.localStorage.getItem(SLASH_DISCOVERY_STORAGE_KEY);
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return { shownCount: 0, completed: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw) as Partial<SlashDiscoveryState>;
|
|
47
|
+
return {
|
|
48
|
+
shownCount:
|
|
49
|
+
typeof parsed.shownCount === "number" && parsed.shownCount >= 0
|
|
50
|
+
? parsed.shownCount
|
|
51
|
+
: 0,
|
|
52
|
+
completed: parsed.completed === true,
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
globalThis.localStorage.removeItem(SLASH_DISCOVERY_STORAGE_KEY);
|
|
56
|
+
return { shownCount: 0, completed: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeState(state: SlashDiscoveryState): void {
|
|
61
|
+
if (!canUseLocalStorage()) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
globalThis.localStorage.setItem(
|
|
65
|
+
SLASH_DISCOVERY_STORAGE_KEY,
|
|
66
|
+
JSON.stringify(state),
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
// Storage unavailable — skip persistence for this page load.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function clearShowTimer() {
|
|
74
|
+
if (showTimer !== null) {
|
|
75
|
+
clearTimeout(showTimer);
|
|
76
|
+
showTimer = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isDesktopEligible(): boolean {
|
|
81
|
+
return (
|
|
82
|
+
typeof globalThis.matchMedia === "function" &&
|
|
83
|
+
globalThis.matchMedia("(min-width: 700px)").matches
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isCompletedOnHost(host: HTMLElement): boolean {
|
|
88
|
+
if (readState().completed) return true;
|
|
89
|
+
return host.dataset.slashCommandDiscovered === "true";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function markHostsCompleted() {
|
|
93
|
+
document
|
|
94
|
+
.querySelectorAll<HTMLElement>("jant-compose-editor")
|
|
95
|
+
.forEach((host) => {
|
|
96
|
+
host.dataset.slashCommandDiscovered = "true";
|
|
97
|
+
host.classList.remove(SLASH_HINT_VISIBLE_CLASS);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function recordExposure(): void {
|
|
102
|
+
const state = readState();
|
|
103
|
+
if (state.completed || exposureRecordedThisSession) return;
|
|
104
|
+
if (state.shownCount >= SLASH_HINT_MAX_SHOW_COUNT) return;
|
|
105
|
+
|
|
106
|
+
exposureRecordedThisSession = true;
|
|
107
|
+
writeState({ ...state, shownCount: state.shownCount + 1 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function canShowHint(host: HTMLElement): boolean {
|
|
111
|
+
if (!isDesktopEligible()) return false;
|
|
112
|
+
if (isCompletedOnHost(host)) return false;
|
|
113
|
+
|
|
114
|
+
const state = readState();
|
|
115
|
+
return (
|
|
116
|
+
state.shownCount < SLASH_HINT_MAX_SHOW_COUNT || exposureRecordedThisSession
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function hideSlashCommandHint(host?: HTMLElement | null): void {
|
|
121
|
+
clearShowTimer();
|
|
122
|
+
|
|
123
|
+
if (host) {
|
|
124
|
+
host.classList.remove(SLASH_HINT_VISIBLE_CLASS);
|
|
125
|
+
} else if (activeHost) {
|
|
126
|
+
activeHost.classList.remove(SLASH_HINT_VISIBLE_CLASS);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!host || host === activeHost) {
|
|
130
|
+
activeHost = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function showHint(host: HTMLElement): void {
|
|
135
|
+
if (!canShowHint(host)) return;
|
|
136
|
+
|
|
137
|
+
hideSlashCommandHint(activeHost);
|
|
138
|
+
activeHost = host;
|
|
139
|
+
host.classList.add(SLASH_HINT_VISIBLE_CLASS);
|
|
140
|
+
recordExposure();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function scheduleSlashCommandHint(host: HTMLElement): void {
|
|
144
|
+
if (!canShowHint(host)) return;
|
|
145
|
+
if (host.classList.contains(SLASH_HINT_VISIBLE_CLASS)) return;
|
|
146
|
+
|
|
147
|
+
clearShowTimer();
|
|
148
|
+
showTimer = setTimeout(() => {
|
|
149
|
+
showHint(host);
|
|
150
|
+
}, SLASH_HINT_DELAY_MS);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function markSlashCommandDiscovered(): void {
|
|
154
|
+
const state = readState();
|
|
155
|
+
if (!state.completed) {
|
|
156
|
+
writeState({
|
|
157
|
+
shownCount: Math.max(state.shownCount, SLASH_HINT_MAX_SHOW_COUNT),
|
|
158
|
+
completed: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
markHostsCompleted();
|
|
163
|
+
hideSlashCommandHint(activeHost);
|
|
164
|
+
|
|
165
|
+
if (typeof globalThis.fetch === "function") {
|
|
166
|
+
void globalThis
|
|
167
|
+
.fetch(SLASH_DISCOVERY_API_PATH, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { Accept: "application/json" },
|
|
170
|
+
credentials: "same-origin",
|
|
171
|
+
})
|
|
172
|
+
.catch(() => {
|
|
173
|
+
// Keep the local completed state even if the sync request fails.
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Initialize once on DOMContentLoaded. The actual scheduling is driven by
|
|
180
|
+
* the compose editor on focus, so this is a no-op placeholder kept for parity
|
|
181
|
+
* with `compose-discovery-bridge` (and to give future call sites a hook).
|
|
182
|
+
*/
|
|
183
|
+
export function initSlashCommandDiscovery(): void {
|
|
184
|
+
// Intentionally empty — the editor host calls scheduleSlashCommandHint /
|
|
185
|
+
// hideSlashCommandHint directly on focus / update.
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const __testOnly = {
|
|
189
|
+
SLASH_DISCOVERY_API_PATH,
|
|
190
|
+
SLASH_DISCOVERY_STORAGE_KEY,
|
|
191
|
+
SLASH_HINT_DELAY_MS,
|
|
192
|
+
SLASH_HINT_MAX_SHOW_COUNT,
|
|
193
|
+
SLASH_HINT_VISIBLE_CLASS,
|
|
194
|
+
readState,
|
|
195
|
+
reset() {
|
|
196
|
+
exposureRecordedThisSession = false;
|
|
197
|
+
clearShowTimer();
|
|
198
|
+
activeHost = null;
|
|
199
|
+
},
|
|
200
|
+
};
|
|
@@ -641,6 +641,11 @@ export const SlashCommands = Extension.create({
|
|
|
641
641
|
editorRef = props.editor;
|
|
642
642
|
currentRange = props.range;
|
|
643
643
|
renderPopup(props.items, (index) => props.command({ index }));
|
|
644
|
+
document.dispatchEvent(
|
|
645
|
+
new CustomEvent("jant:slash-command-discovered", {
|
|
646
|
+
bubbles: true,
|
|
647
|
+
}),
|
|
648
|
+
);
|
|
644
649
|
|
|
645
650
|
// Append inside the closest dialog (top-layer) or body
|
|
646
651
|
const editorEl = getEditorElement(props.editor);
|
package/src/client-auth.ts
CHANGED
|
@@ -14,6 +14,7 @@ import "./client/components/jant-compose-editor.js";
|
|
|
14
14
|
import "./client/components/jant-compose-fullscreen.js";
|
|
15
15
|
import "./client/compose-bridge.js";
|
|
16
16
|
import "./client/compose-discovery-bridge.js";
|
|
17
|
+
import "./client/slash-discovery-bridge.js";
|
|
17
18
|
import "./client/compose-shortcuts.js";
|
|
18
19
|
import "./client/components/jant-settings-general.js";
|
|
19
20
|
import "./client/components/jant-settings-avatar.js";
|
|
@@ -2131,6 +2131,11 @@ msgstr "Transparent square"
|
|
|
2131
2131
|
msgid "Tune color in a real reading context"
|
|
2132
2132
|
msgstr "Tune color in a real reading context"
|
|
2133
2133
|
|
|
2134
|
+
#. @context: First-use hint shown over the compose editor to surface the slash command menu
|
|
2135
|
+
#: src/ui/compose/ComposeDialog.tsx
|
|
2136
|
+
msgid "Type / for commands"
|
|
2137
|
+
msgstr "Type / for commands"
|
|
2138
|
+
|
|
2134
2139
|
#. @context: Compose quote text placeholder
|
|
2135
2140
|
#: src/ui/compose/ComposeDialog.tsx
|
|
2136
2141
|
msgid "Type the quote..."
|