@jant/core 0.6.15 → 0.6.16
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 +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ComposeFormat,
|
|
27
27
|
ComposeLabels,
|
|
28
28
|
ComposeAttachment,
|
|
29
|
+
ComposeRowStatus,
|
|
29
30
|
AttachedTextItem,
|
|
30
31
|
ComposeEditorSelection,
|
|
31
32
|
ComposeFullscreenOpenDetail,
|
|
@@ -97,43 +98,44 @@ const COMPOSE_TOOLBAR_ICONS = {
|
|
|
97
98
|
/>
|
|
98
99
|
<path
|
|
99
100
|
d="m9 1.95 2.08 4.21 4.65.67-3.36 3.29.8 4.63L9 12.55l-4.17 2.2.8-4.63-3.36-3.29 4.65-.67z"
|
|
100
|
-
stroke-width="1.6"
|
|
101
101
|
/>
|
|
102
102
|
`,
|
|
103
|
+
/* The one filled glyph in the row, so its bars are cut to the weight the
|
|
104
|
+
stroked icons read at — a 2.05 bar beside a 1.42 stroke looks bold. */
|
|
103
105
|
title: `
|
|
104
106
|
<rect
|
|
105
107
|
x="3.35"
|
|
106
|
-
y="3.
|
|
108
|
+
y="3.37"
|
|
107
109
|
width="11.3"
|
|
108
|
-
height="
|
|
109
|
-
rx="0.
|
|
110
|
+
height="1.72"
|
|
111
|
+
rx="0.6"
|
|
110
112
|
fill="currentColor"
|
|
111
113
|
stroke="none"
|
|
112
114
|
/>
|
|
113
115
|
<rect
|
|
114
|
-
x="7.
|
|
116
|
+
x="7.99"
|
|
115
117
|
y="4.6"
|
|
116
|
-
width="2.
|
|
118
|
+
width="2.02"
|
|
117
119
|
height="9.45"
|
|
118
|
-
rx="0.
|
|
120
|
+
rx="0.68"
|
|
119
121
|
fill="currentColor"
|
|
120
122
|
stroke="none"
|
|
121
123
|
/>
|
|
122
124
|
<rect
|
|
123
125
|
x="6.75"
|
|
124
|
-
y="13.
|
|
126
|
+
y="13.32"
|
|
125
127
|
width="4.5"
|
|
126
|
-
height="
|
|
127
|
-
rx="0.
|
|
128
|
+
height="0.95"
|
|
129
|
+
rx="0.48"
|
|
128
130
|
fill="currentColor"
|
|
129
131
|
stroke="none"
|
|
130
132
|
/>
|
|
131
133
|
`,
|
|
132
134
|
fullscreen: `
|
|
133
|
-
<path d="M5.85 3H3v2.85"
|
|
134
|
-
<path d="M12.15 3H15v2.85"
|
|
135
|
-
<path d="M3 12.15V15h2.85"
|
|
136
|
-
<path d="M15 12.15V15h-2.85"
|
|
135
|
+
<path d="M5.85 3H3v2.85" />
|
|
136
|
+
<path d="M12.15 3H15v2.85" />
|
|
137
|
+
<path d="M3 12.15V15h2.85" />
|
|
138
|
+
<path d="M15 12.15V15h-2.85" />
|
|
137
139
|
`,
|
|
138
140
|
} as const;
|
|
139
141
|
|
|
@@ -147,7 +149,6 @@ function renderComposeToolbarIcon(
|
|
|
147
149
|
viewBox="0 0 18 18"
|
|
148
150
|
fill="none"
|
|
149
151
|
stroke="currentColor"
|
|
150
|
-
stroke-width="1.55"
|
|
151
152
|
stroke-linecap="round"
|
|
152
153
|
stroke-linejoin="round"
|
|
153
154
|
aria-hidden="true"
|
|
@@ -189,6 +190,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
189
190
|
threadItem: { type: Boolean, attribute: "thread-item" },
|
|
190
191
|
removable: { type: Boolean },
|
|
191
192
|
inlineFormat: { type: Boolean, attribute: "inline-format" },
|
|
193
|
+
titleByDefault: { type: Boolean, attribute: "title-by-default" },
|
|
194
|
+
positionLabel: { type: String, attribute: "position-label" },
|
|
195
|
+
badgeLabel: { type: String, attribute: "badge-label" },
|
|
196
|
+
headerExtra: { attribute: false },
|
|
192
197
|
slashCommandDiscovered: { type: Boolean },
|
|
193
198
|
_title: { state: true },
|
|
194
199
|
_bodyJson: { state: true },
|
|
@@ -215,6 +220,29 @@ export class JantComposeEditor extends LitElement {
|
|
|
215
220
|
declare threadItem: boolean;
|
|
216
221
|
declare removable: boolean;
|
|
217
222
|
declare inlineFormat: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Whether a note's title field starts visible. A post that opens a thread is
|
|
225
|
+
* usually the one worth naming, so it defaults on there; a reply or a
|
|
226
|
+
* continuation post carries the thread's title already, so it defaults off.
|
|
227
|
+
* On a new single post the owner narrows it further with the author's own
|
|
228
|
+
* remembered answer. Either way the `T` tool overrides it, and a post that
|
|
229
|
+
* arrives with a title always shows the field.
|
|
230
|
+
*/
|
|
231
|
+
declare titleByDefault: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* "1/3"-style position marker shown before the format selector. Empty for a
|
|
234
|
+
* single post, where there is no "which one" to answer.
|
|
235
|
+
*/
|
|
236
|
+
declare positionLabel: string;
|
|
237
|
+
/** Status marker for the header row, e.g. "Editing". Mutually exclusive with
|
|
238
|
+
* `positionLabel` in practice — a thread post is never an edit. */
|
|
239
|
+
declare badgeLabel: string;
|
|
240
|
+
/**
|
|
241
|
+
* Owner-supplied content for the right side of the format header — the
|
|
242
|
+
* dialog's per-post date/permalink control. Passed in rather than built here
|
|
243
|
+
* because publish metadata belongs to the dialog, not to the editor.
|
|
244
|
+
*/
|
|
245
|
+
declare headerExtra: unknown;
|
|
218
246
|
declare slashCommandDiscovered: boolean;
|
|
219
247
|
declare _title: string;
|
|
220
248
|
declare _bodyJson: JSONContent | null;
|
|
@@ -243,7 +271,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
243
271
|
private _emojiContainer: HTMLElement | null = null;
|
|
244
272
|
private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
|
|
245
273
|
private _onDocClickBound = this._onDocumentClick.bind(this);
|
|
246
|
-
private _scrollBufferApplied = false;
|
|
247
274
|
private _filePickerCleanup: (() => void) | null = null;
|
|
248
275
|
private _suppressAttachedTextOpenUntil = 0;
|
|
249
276
|
/**
|
|
@@ -273,6 +300,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
273
300
|
this.threadItem = false;
|
|
274
301
|
this.removable = false;
|
|
275
302
|
this.inlineFormat = false;
|
|
303
|
+
this.titleByDefault = true;
|
|
304
|
+
this.positionLabel = "";
|
|
305
|
+
this.badgeLabel = "";
|
|
306
|
+
this.headerExtra = null;
|
|
276
307
|
this.slashCommandDiscovered = false;
|
|
277
308
|
this._title = "";
|
|
278
309
|
this._bodyJson = null;
|
|
@@ -280,7 +311,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
280
311
|
this._quoteText = "";
|
|
281
312
|
this._quoteAuthor = "";
|
|
282
313
|
this._rating = 0;
|
|
283
|
-
this._showTitle =
|
|
314
|
+
this._showTitle = true;
|
|
284
315
|
this._showRating = false;
|
|
285
316
|
this._attachedTexts = [];
|
|
286
317
|
this._attachments = [];
|
|
@@ -778,9 +809,90 @@ export class JantComposeEditor extends LitElement {
|
|
|
778
809
|
}
|
|
779
810
|
}
|
|
780
811
|
|
|
812
|
+
/**
|
|
813
|
+
* What this row contributes to the composer's submit state.
|
|
814
|
+
*
|
|
815
|
+
* The editor owns these fields, so it owns the answer: the dialog reduces
|
|
816
|
+
* `getStatus()` over the rows it has rather than reading editors back out of
|
|
817
|
+
* the DOM, which during a render still holds the row it is about to remove.
|
|
818
|
+
*
|
|
819
|
+
* @returns Whether the row holds anything worth keeping, and whether it is
|
|
820
|
+
* complete enough to publish
|
|
821
|
+
* @example
|
|
822
|
+
* editor.getStatus(); // { hasContent: true, publishable: false } — link, no URL yet
|
|
823
|
+
*/
|
|
824
|
+
getStatus(): ComposeRowStatus {
|
|
825
|
+
const data = this.getData();
|
|
826
|
+
const hasContent =
|
|
827
|
+
!!data.body ||
|
|
828
|
+
!!data.title.trim() ||
|
|
829
|
+
!!data.url.trim() ||
|
|
830
|
+
!!data.quoteText.trim() ||
|
|
831
|
+
!!data.quoteAuthor.trim() ||
|
|
832
|
+
data.rating > 0 ||
|
|
833
|
+
data.attachments.length > 0 ||
|
|
834
|
+
data.attachedTexts.length > 0;
|
|
835
|
+
const fieldsValid =
|
|
836
|
+
!this.getUrlValidationMessage() && !this.getLinkTitleValidationMessage();
|
|
837
|
+
|
|
838
|
+
let publishable: boolean;
|
|
839
|
+
switch (this.format) {
|
|
840
|
+
// A link is its URL and title, both of which the validators already
|
|
841
|
+
// require — nothing else has to be filled in.
|
|
842
|
+
case "link":
|
|
843
|
+
publishable = fieldsValid;
|
|
844
|
+
break;
|
|
845
|
+
case "quote":
|
|
846
|
+
publishable = fieldsValid && !!data.quoteText.trim();
|
|
847
|
+
break;
|
|
848
|
+
default:
|
|
849
|
+
publishable = fieldsValid && hasContent;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return { hasContent, publishable };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
#lastStatus: ComposeRowStatus | null = null;
|
|
856
|
+
|
|
857
|
+
/** Tell the owner when — and only when — this row's answer changes. */
|
|
858
|
+
#notifyStatus() {
|
|
859
|
+
const status = this.getStatus();
|
|
860
|
+
if (
|
|
861
|
+
this.#lastStatus?.hasContent === status.hasContent &&
|
|
862
|
+
this.#lastStatus?.publishable === status.publishable
|
|
863
|
+
) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.#lastStatus = status;
|
|
867
|
+
this.dispatchEvent(
|
|
868
|
+
new CustomEvent<ComposeRowStatus>("jant:compose-status", {
|
|
869
|
+
bubbles: true,
|
|
870
|
+
detail: status,
|
|
871
|
+
}),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Set the title toggle's default and apply it now.
|
|
877
|
+
*
|
|
878
|
+
* `titleByDefault` alone only takes effect on the first update or on
|
|
879
|
+
* `reset()`, and an owner that resets before Lit re-renders would seed from
|
|
880
|
+
* the previous session's value. An owner that knows the default up front
|
|
881
|
+
* calls this instead.
|
|
882
|
+
*
|
|
883
|
+
* @param show - Whether a note's title field starts visible
|
|
884
|
+
* @example
|
|
885
|
+
* editor.setTitleDefault(false); // next new note opens without a title field
|
|
886
|
+
*/
|
|
887
|
+
setTitleDefault(show: boolean) {
|
|
888
|
+
this.titleByDefault = show;
|
|
889
|
+
this._showTitle = show;
|
|
890
|
+
}
|
|
891
|
+
|
|
781
892
|
reset() {
|
|
782
893
|
this.#clearPendingInlineImageUploads();
|
|
783
894
|
this._title = "";
|
|
895
|
+
this._showTitle = this.titleByDefault;
|
|
784
896
|
this._bodyJson = null;
|
|
785
897
|
this._editor?.commands.clearContent();
|
|
786
898
|
this._lastEditorSelection = null;
|
|
@@ -788,7 +900,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
788
900
|
this._quoteText = "";
|
|
789
901
|
this._quoteAuthor = "";
|
|
790
902
|
this._rating = 0;
|
|
791
|
-
this._showTitle = false;
|
|
792
903
|
this._showRating = false;
|
|
793
904
|
this._attachedTexts = [];
|
|
794
905
|
// Revoke preview URLs before clearing
|
|
@@ -811,9 +922,12 @@ export class JantComposeEditor extends LitElement {
|
|
|
811
922
|
status: ComposeAttachment["status"],
|
|
812
923
|
mediaId: string | null,
|
|
813
924
|
error: string | null,
|
|
925
|
+
remoteUrl?: string | null,
|
|
814
926
|
) {
|
|
815
927
|
this._attachments = this._attachments.map((a) =>
|
|
816
|
-
a.clientId === clientId
|
|
928
|
+
a.clientId === clientId
|
|
929
|
+
? { ...a, status, mediaId, error, remoteUrl: remoteUrl ?? a.remoteUrl }
|
|
930
|
+
: a,
|
|
817
931
|
);
|
|
818
932
|
}
|
|
819
933
|
|
|
@@ -978,7 +1092,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
978
1092
|
toolbarMode: "compose",
|
|
979
1093
|
onUpdate: (json) => {
|
|
980
1094
|
this._bodyJson = json;
|
|
981
|
-
this._ensureScrollBuffer();
|
|
982
1095
|
this.promoteLeadingH1Title();
|
|
983
1096
|
hideSlashCommandHint(this);
|
|
984
1097
|
},
|
|
@@ -1009,36 +1122,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
1009
1122
|
tableControlLabels: this.labels.tableControls,
|
|
1010
1123
|
});
|
|
1011
1124
|
this._lastEditorSelection = this._readEditorSelection();
|
|
1012
|
-
|
|
1013
|
-
// Lock editor min-height once so new lines fill existing space
|
|
1014
|
-
// instead of growing the dialog line-by-line.
|
|
1015
|
-
// Skip in page mode where the editor grows freely with the page.
|
|
1016
|
-
this._scrollBufferApplied = false;
|
|
1017
|
-
const dom = this._editor.view.dom as HTMLElement;
|
|
1018
|
-
const isPageMode = !!this.closest(".compose-page-shell");
|
|
1019
|
-
if (!isPageMode) {
|
|
1020
|
-
const last = dom.lastElementChild as HTMLElement | null;
|
|
1021
|
-
const contentH = last ? last.offsetTop + last.offsetHeight : 0;
|
|
1022
|
-
const buffer = this.format !== "note" ? 60 : 120;
|
|
1023
|
-
dom.style.minHeight = `${contentH + buffer}px`;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* One-time: adds bottom padding for scroll buffer once the
|
|
1029
|
-
* compose-body starts scrolling. Since the dialog is already at
|
|
1030
|
-
* max-height by that point, the extra padding doesn't grow it.
|
|
1031
|
-
*/
|
|
1032
|
-
private _ensureScrollBuffer() {
|
|
1033
|
-
if (this._scrollBufferApplied) return;
|
|
1034
|
-
const dom = this._editor?.view?.dom as HTMLElement | undefined;
|
|
1035
|
-
if (!dom) return;
|
|
1036
|
-
const body = this.querySelector(".compose-body") as HTMLElement | null;
|
|
1037
|
-
if (!body) return;
|
|
1038
|
-
if (body.scrollHeight > body.clientHeight + 20) {
|
|
1039
|
-
dom.style.paddingBottom = "40px";
|
|
1040
|
-
this._scrollBufferApplied = true;
|
|
1041
|
-
}
|
|
1042
1125
|
}
|
|
1043
1126
|
|
|
1044
1127
|
private _destroyEditor() {
|
|
@@ -1061,6 +1144,17 @@ export class JantComposeEditor extends LitElement {
|
|
|
1061
1144
|
"_attachmentOrder",
|
|
1062
1145
|
]);
|
|
1063
1146
|
|
|
1147
|
+
protected willUpdate(changed: Map<string, unknown>) {
|
|
1148
|
+
super.willUpdate(changed);
|
|
1149
|
+
|
|
1150
|
+
// `titleByDefault` is an owner-supplied prop, so it is only known once the
|
|
1151
|
+
// first update runs. Seed the toggle from it there and leave it alone after
|
|
1152
|
+
// that — from then on the value belongs to the user (and to `reset()`).
|
|
1153
|
+
if (!this.hasUpdated) {
|
|
1154
|
+
this._showTitle = this.titleByDefault;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1064
1158
|
protected updated(changed: Map<string, unknown>) {
|
|
1065
1159
|
super.updated(changed);
|
|
1066
1160
|
|
|
@@ -1098,6 +1192,11 @@ export class JantComposeEditor extends LitElement {
|
|
|
1098
1192
|
}
|
|
1099
1193
|
}
|
|
1100
1194
|
|
|
1195
|
+
// Every field that can change this row's answer is reactive, so `updated()`
|
|
1196
|
+
// is where the owner hears about it — including the ones a user edit never
|
|
1197
|
+
// touches, like an upload finishing.
|
|
1198
|
+
this.#notifyStatus();
|
|
1199
|
+
|
|
1101
1200
|
// Notify parent dialog of content changes for draft auto-save. A format
|
|
1102
1201
|
// conversion writes content fields too, but it's not a user edit, so skip
|
|
1103
1202
|
// the notification once when asked.
|
|
@@ -1171,6 +1270,8 @@ export class JantComposeEditor extends LitElement {
|
|
|
1171
1270
|
showRating?: boolean;
|
|
1172
1271
|
media?: Array<{
|
|
1173
1272
|
id: string;
|
|
1273
|
+
/** Preserved so a restored draft's attachmentOrder still maps by clientId */
|
|
1274
|
+
clientId?: string;
|
|
1174
1275
|
previewUrl: string;
|
|
1175
1276
|
posterUrl?: string | null;
|
|
1176
1277
|
alt?: string;
|
|
@@ -1196,6 +1297,8 @@ export class JantComposeEditor extends LitElement {
|
|
|
1196
1297
|
this._rating = data.rating;
|
|
1197
1298
|
this._showRating = true;
|
|
1198
1299
|
}
|
|
1300
|
+
// A post that already has a title shows the field whatever the default is —
|
|
1301
|
+
// otherwise editing a titled reply would silently drop its title.
|
|
1199
1302
|
if (data.showTitle !== undefined) this._showTitle = data.showTitle;
|
|
1200
1303
|
else if (data.title && data.format === "note") this._showTitle = true;
|
|
1201
1304
|
if (data.showRating !== undefined) this._showRating = data.showRating;
|
|
@@ -1217,13 +1320,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
1217
1320
|
// Convert media attachments to ComposeAttachment[] with status "done"
|
|
1218
1321
|
if (data.media?.length) {
|
|
1219
1322
|
const attachments = data.media.map((m) => ({
|
|
1220
|
-
clientId: randomUUID(),
|
|
1323
|
+
clientId: m.clientId ?? randomUUID(),
|
|
1221
1324
|
file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
|
|
1222
1325
|
previewUrl: m.previewUrl,
|
|
1223
1326
|
posterUrl: m.posterUrl ?? null,
|
|
1224
1327
|
status: "done" as const,
|
|
1225
1328
|
progress: null,
|
|
1226
1329
|
mediaId: m.id,
|
|
1330
|
+
remoteUrl: m.previewUrl,
|
|
1227
1331
|
alt: m.alt ?? "",
|
|
1228
1332
|
error: null,
|
|
1229
1333
|
summary: m.summary ?? null,
|
|
@@ -1281,6 +1385,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1281
1385
|
|
|
1282
1386
|
this._showUrlValidation = false;
|
|
1283
1387
|
this._showLinkTitleValidation = false;
|
|
1388
|
+
this.#notifyStatus();
|
|
1284
1389
|
}
|
|
1285
1390
|
|
|
1286
1391
|
/** Updates editor content and title from fullscreen close */
|
|
@@ -1304,6 +1409,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
1304
1409
|
);
|
|
1305
1410
|
}
|
|
1306
1411
|
this._lastEditorSelection = selection ?? this._readEditorSelection();
|
|
1412
|
+
// Content handed in from outside has to be visible to the owner in the same
|
|
1413
|
+
// tick: fullscreen's ⌘↵ hands the text back and publishes on the next line,
|
|
1414
|
+
// with no render in between to carry the news.
|
|
1415
|
+
this.#notifyStatus();
|
|
1307
1416
|
}
|
|
1308
1417
|
|
|
1309
1418
|
private static SUMMARY_LENGTH = 100;
|
|
@@ -1528,6 +1637,25 @@ export class JantComposeEditor extends LitElement {
|
|
|
1528
1637
|
el.style.height = `${el.scrollHeight}px`;
|
|
1529
1638
|
}
|
|
1530
1639
|
|
|
1640
|
+
/**
|
|
1641
|
+
* Puts the caret in the body when the click landed on blank section instead
|
|
1642
|
+
* of on the editor. The surfaces that reserve height — the `/new` page, the
|
|
1643
|
+
* dialog on a phone — leave a stretch of empty paper under a two-line
|
|
1644
|
+
* editor, and it should behave like paper: click it, write there. Only the
|
|
1645
|
+
* layout wrappers forward; a click that reached a real control belongs to
|
|
1646
|
+
* that control.
|
|
1647
|
+
*/
|
|
1648
|
+
private _focusBodyFromBlankSpace = (e: MouseEvent) => {
|
|
1649
|
+
const target = e.target as HTMLElement | null;
|
|
1650
|
+
if (!target) return;
|
|
1651
|
+
const forwards =
|
|
1652
|
+
target.classList.contains("compose-body") ||
|
|
1653
|
+
target.classList.contains("compose-field-enter") ||
|
|
1654
|
+
target.classList.contains("compose-tiptap-wrap");
|
|
1655
|
+
if (!forwards) return;
|
|
1656
|
+
this._editor?.commands.focus("end");
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1531
1659
|
private _setRating(star: number) {
|
|
1532
1660
|
this._rating = this._rating === star ? 0 : star;
|
|
1533
1661
|
}
|
|
@@ -1588,6 +1716,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1588
1716
|
status: "pending",
|
|
1589
1717
|
progress: null,
|
|
1590
1718
|
mediaId: null,
|
|
1719
|
+
remoteUrl: null,
|
|
1591
1720
|
alt: "",
|
|
1592
1721
|
error: null,
|
|
1593
1722
|
summary: null,
|
|
@@ -1978,28 +2107,15 @@ export class JantComposeEditor extends LitElement {
|
|
|
1978
2107
|
return html`
|
|
1979
2108
|
<div class="compose-field-enter">
|
|
1980
2109
|
${this._showTitle
|
|
1981
|
-
? html
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
placeholder=${this.labels.titlePlaceholder}
|
|
1991
|
-
/>
|
|
1992
|
-
<button
|
|
1993
|
-
type="button"
|
|
1994
|
-
class="compose-note-title-dismiss"
|
|
1995
|
-
@click=${() => {
|
|
1996
|
-
this._showTitle = false;
|
|
1997
|
-
}}
|
|
1998
|
-
>
|
|
1999
|
-
✕
|
|
2000
|
-
</button>
|
|
2001
|
-
</div>
|
|
2002
|
-
`
|
|
2110
|
+
? html`<input
|
|
2111
|
+
type="text"
|
|
2112
|
+
.value=${this._title}
|
|
2113
|
+
@input=${(e: Event) => this._onInput("_title", e)}
|
|
2114
|
+
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2115
|
+
@keydown=${this._handleTitleKeydown}
|
|
2116
|
+
class="compose-input compose-note-title"
|
|
2117
|
+
placeholder=${this.labels.titlePlaceholder}
|
|
2118
|
+
/>`
|
|
2003
2119
|
: nothing}
|
|
2004
2120
|
<div class="compose-tiptap-wrap">
|
|
2005
2121
|
<div class="compose-tiptap-body"></div>
|
|
@@ -2076,9 +2192,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2076
2192
|
: nothing}
|
|
2077
2193
|
<div class="compose-divider"></div>
|
|
2078
2194
|
<div class="compose-tiptap-wrap">
|
|
2079
|
-
<div
|
|
2080
|
-
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-link"
|
|
2081
|
-
></div>
|
|
2195
|
+
<div class="compose-tiptap-body compose-tiptap-link"></div>
|
|
2082
2196
|
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
2083
2197
|
${this.labels.slashHint}
|
|
2084
2198
|
</span>
|
|
@@ -2102,7 +2216,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2102
2216
|
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
2103
2217
|
class="compose-input compose-quote-text"
|
|
2104
2218
|
placeholder=${this.labels.quotePlaceholder}
|
|
2105
|
-
rows="
|
|
2219
|
+
rows="4"
|
|
2106
2220
|
></textarea>
|
|
2107
2221
|
</div>
|
|
2108
2222
|
<div class="compose-quote-author-row">
|
|
@@ -2147,9 +2261,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2147
2261
|
aria-hidden="true"
|
|
2148
2262
|
></div>
|
|
2149
2263
|
<div class="compose-tiptap-wrap">
|
|
2150
|
-
<div
|
|
2151
|
-
class="compose-tiptap-body compose-tiptap-thoughts compose-tiptap-thoughts-quote"
|
|
2152
|
-
></div>
|
|
2264
|
+
<div class="compose-tiptap-body"></div>
|
|
2153
2265
|
<span class="compose-slash-discovery-hint" aria-hidden="true">
|
|
2154
2266
|
${this.labels.slashHint}
|
|
2155
2267
|
</span>
|
|
@@ -2605,12 +2717,13 @@ export class JantComposeEditor extends LitElement {
|
|
|
2605
2717
|
"compose-tool-btn-active": this._showEmojiPicker,
|
|
2606
2718
|
})}
|
|
2607
2719
|
title=${this.labels.emoji}
|
|
2720
|
+
aria-pressed=${this._showEmojiPicker ? "true" : "false"}
|
|
2608
2721
|
@click=${() => this._toggleEmojiPicker()}
|
|
2609
2722
|
>
|
|
2610
2723
|
${renderComposeToolbarIcon(COMPOSE_TOOLBAR_ICONS.emoji)}
|
|
2611
2724
|
</button>
|
|
2612
2725
|
|
|
2613
|
-
<div class="compose-tool-
|
|
2726
|
+
<div class="compose-tool-gap"></div>
|
|
2614
2727
|
|
|
2615
2728
|
<button
|
|
2616
2729
|
type="button"
|
|
@@ -2619,6 +2732,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2619
2732
|
"compose-tool-btn-active": this._showRating,
|
|
2620
2733
|
})}
|
|
2621
2734
|
title=${this.labels.rate}
|
|
2735
|
+
aria-pressed=${this._showRating ? "true" : "false"}
|
|
2622
2736
|
@click=${() => {
|
|
2623
2737
|
const willShow = !this._showRating;
|
|
2624
2738
|
this._showRating = willShow;
|
|
@@ -2643,9 +2757,20 @@ export class JantComposeEditor extends LitElement {
|
|
|
2643
2757
|
"compose-tool-btn-active": this._showTitle,
|
|
2644
2758
|
})}
|
|
2645
2759
|
title=${this.labels.title}
|
|
2760
|
+
aria-pressed=${this._showTitle ? "true" : "false"}
|
|
2646
2761
|
@click=${() => {
|
|
2647
2762
|
const willShow = !this._showTitle;
|
|
2648
2763
|
this._showTitle = willShow;
|
|
2764
|
+
// Only this deliberate click is worth remembering. The other
|
|
2765
|
+
// writers of `_showTitle` — H1 promotion, format conversion,
|
|
2766
|
+
// fullscreen reveal — are the program acting on one post, not
|
|
2767
|
+
// the author stating a habit, so they stay silent.
|
|
2768
|
+
this.dispatchEvent(
|
|
2769
|
+
new CustomEvent("jant:title-toggle", {
|
|
2770
|
+
bubbles: true,
|
|
2771
|
+
detail: { showTitle: willShow },
|
|
2772
|
+
}),
|
|
2773
|
+
);
|
|
2649
2774
|
if (willShow) {
|
|
2650
2775
|
this.updateComplete.then(() => {
|
|
2651
2776
|
this.querySelector<HTMLInputElement>(
|
|
@@ -2689,14 +2814,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
2689
2814
|
|
|
2690
2815
|
return html`
|
|
2691
2816
|
<div class="compose-thread-post-header">
|
|
2692
|
-
<div class="compose-segmented
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
"compose-format-pill-link": this.format === "link",
|
|
2697
|
-
"compose-format-pill-quote": this.format === "quote",
|
|
2698
|
-
})}
|
|
2699
|
-
></div>
|
|
2817
|
+
<div class="compose-segmented">
|
|
2818
|
+
<!-- No sliding indicator: the items size to their labels now, so a
|
|
2819
|
+
thirds-based pill would not line up. The active item carries its
|
|
2820
|
+
own background instead. -->
|
|
2700
2821
|
${formats.map(
|
|
2701
2822
|
(f) => html`
|
|
2702
2823
|
<button
|
|
@@ -2721,6 +2842,17 @@ export class JantComposeEditor extends LitElement {
|
|
|
2721
2842
|
`,
|
|
2722
2843
|
)}
|
|
2723
2844
|
</div>
|
|
2845
|
+
${this.positionLabel
|
|
2846
|
+
? html`<span class="compose-post-position"
|
|
2847
|
+
>${this.positionLabel}</span
|
|
2848
|
+
>`
|
|
2849
|
+
: nothing}
|
|
2850
|
+
${this.badgeLabel
|
|
2851
|
+
? html`<span class="compose-post-position compose-post-badge"
|
|
2852
|
+
>${this.badgeLabel}</span
|
|
2853
|
+
>`
|
|
2854
|
+
: nothing}
|
|
2855
|
+
${this.headerExtra ?? nothing}
|
|
2724
2856
|
${this.removable
|
|
2725
2857
|
? html`<button
|
|
2726
2858
|
type="button"
|
|
@@ -2762,11 +2894,11 @@ export class JantComposeEditor extends LitElement {
|
|
|
2762
2894
|
}
|
|
2763
2895
|
|
|
2764
2896
|
render() {
|
|
2897
|
+
// The format selector lives here in every mode — single post, reply, and
|
|
2898
|
+
// thread item alike — so there is exactly one place it can be.
|
|
2765
2899
|
return html`
|
|
2766
|
-
${this.
|
|
2767
|
-
|
|
2768
|
-
: nothing}
|
|
2769
|
-
<section class="compose-body">
|
|
2900
|
+
${this._renderFormatHeader()}
|
|
2901
|
+
<section class="compose-body" @click=${this._focusBodyFromBlankSpace}>
|
|
2770
2902
|
${this.format === "note"
|
|
2771
2903
|
? this._renderNoteFields()
|
|
2772
2904
|
: this.format === "link"
|
|
@@ -59,7 +59,7 @@ export class JantComposeFullscreen extends LitElement {
|
|
|
59
59
|
this.labels = {} as ComposeLabels;
|
|
60
60
|
this._open = false;
|
|
61
61
|
this._title = "";
|
|
62
|
-
this._showTitle =
|
|
62
|
+
this._showTitle = true;
|
|
63
63
|
this._replyContext = null;
|
|
64
64
|
this._replyExpanded = false;
|
|
65
65
|
}
|
|
@@ -314,9 +314,7 @@ export class JantComposeFullscreen extends LitElement {
|
|
|
314
314
|
class=${placeholderClasses}
|
|
315
315
|
@click=${() => this._revealTitle()}
|
|
316
316
|
>
|
|
317
|
-
${
|
|
318
|
-
? this.labels.titlePlaceholder || this.labels.title || "Title"
|
|
319
|
-
: this.labels.title || "Title"}
|
|
317
|
+
${this.labels.titlePlaceholder || this.labels.title || "Title"}
|
|
320
318
|
</button>
|
|
321
319
|
`;
|
|
322
320
|
}
|
|
@@ -412,7 +410,7 @@ export class JantComposeFullscreen extends LitElement {
|
|
|
412
410
|
</button>
|
|
413
411
|
</div>
|
|
414
412
|
<div
|
|
415
|
-
class="compose-editor-row compose-fullscreen-editor-row"
|
|
413
|
+
class="compose-editor-row compose-fullscreen-editor-row is-current"
|
|
416
414
|
>
|
|
417
415
|
<div class="compose-thread-dot"></div>
|
|
418
416
|
${editorSurface("reply")}
|