@jant/core 0.3.44 → 0.3.46
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 +40 -39
- package/dist/app-CM7sb3xO.js +5 -0
- package/dist/{app-CtJDxZBb.js → app-DB-P66E5.js} +147 -203
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-DDs6NzB3.css +2 -0
- package/dist/client/_assets/{client-auth-CXILhW1b.js → client-auth-BLCUje4M.js} +193 -174
- package/dist/client/_assets/{client-D95FNDg5.js → client-dSfWfMe9.js} +7 -7
- package/dist/{github-sync-7y_nTXx1.js → github-sync-CQ1x271f.js} +3 -0
- package/dist/index.js +4 -87
- package/dist/node.js +3 -3
- package/package.json +1 -1
- package/src/__tests__/import-site-command.test.ts +18 -0
- package/src/client/components/jant-compose-dialog.ts +94 -15
- package/src/client/components/jant-compose-editor.ts +11 -6
- package/src/client/components/jant-post-menu.ts +23 -5
- package/src/client/compose-bridge.ts +2 -1
- package/src/client/random-uuid.ts +23 -0
- package/src/client/toast.ts +29 -2
- package/src/client/upload-session.ts +1 -1
- package/src/db/migrations/0020_free_zaladane.sql +1 -0
- package/src/db/migrations/meta/0020_snapshot.json +2129 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0018_red_warlock.sql +1 -0
- package/src/db/migrations/pg/meta/0018_snapshot.json +2739 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +0 -30
- package/src/db/schema.ts +0 -39
- package/src/i18n/locales/public/en.po +10 -5
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +10 -5
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +10 -5
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/index.ts +0 -3
- package/src/lib/__tests__/resolve-config.test.ts +4 -4
- package/src/lib/__tests__/startup-config.test.ts +27 -2
- package/src/lib/constants.ts +1 -0
- package/src/lib/github-sync-trigger.ts +7 -51
- package/src/lib/startup-config.ts +53 -6
- package/src/routes/api/github-sync.tsx +36 -14
- package/src/routes/pages/home.tsx +2 -0
- package/src/routes/pages/latest.tsx +2 -0
- package/src/runtime/__tests__/readiness.test.ts +34 -0
- package/src/runtime/readiness.ts +8 -4
- package/src/services/__tests__/collection.test.ts +13 -11
- package/src/services/github-sync.ts +6 -0
- package/src/styles/components.css +14 -0
- package/src/styles/ui.css +97 -0
- package/src/types/bindings.ts +0 -2
- package/src/types/config.ts +1 -1
- package/src/types/props.ts +2 -0
- package/src/ui/__tests__/font-themes.test.ts +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -17
- package/src/ui/font-themes.ts +17 -17
- package/src/ui/pages/HomePage.tsx +18 -5
- package/dist/app-BI9bnCkO.js +0 -5
- package/dist/client/_assets/client-BQH7AQ24.css +0 -2
- package/src/lib/github-sync-queue-handler.ts +0 -69
- package/src/lib/github-sync-worker.ts +0 -72
- package/src/lib/job-queue-cf.ts +0 -18
- package/src/lib/job-queue-db.ts +0 -149
- package/src/lib/job-queue.ts +0 -35
|
@@ -40,6 +40,7 @@ import { getMediaCategory } from "../../lib/upload.js";
|
|
|
40
40
|
import { getSlugValidationIssue } from "../../lib/slug-format.js";
|
|
41
41
|
import { createTiptapEditor } from "../tiptap/create-editor.js";
|
|
42
42
|
import { MAX_THREAD_POSTS } from "../../types.js";
|
|
43
|
+
import { randomUUID } from "../random-uuid.js";
|
|
43
44
|
|
|
44
45
|
interface ReplyToMedia {
|
|
45
46
|
url: string;
|
|
@@ -641,6 +642,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
641
642
|
private _suppressBeforeUnload = false;
|
|
642
643
|
private _dialogEl: HTMLDialogElement | null = null;
|
|
643
644
|
private _mousedownOnBackdrop = false;
|
|
645
|
+
private _mousedownPos: { x: number; y: number } | null = null;
|
|
644
646
|
private _filePickerActive = false;
|
|
645
647
|
private _ignoreNextEscapeClose = false;
|
|
646
648
|
private _openEditRequestId = 0;
|
|
@@ -2264,13 +2266,36 @@ export class JantComposeDialog extends LitElement {
|
|
|
2264
2266
|
this.requestClose();
|
|
2265
2267
|
};
|
|
2266
2268
|
|
|
2269
|
+
// Returns true if the given point is inside any open top-layer popover.
|
|
2270
|
+
// Browsers sometimes fire backdrop click events even when the pointer is
|
|
2271
|
+
// over a popover that is rendered above the dialog in the top layer —
|
|
2272
|
+
// document.elementFromPoint() ignores the top layer, so we check bounding
|
|
2273
|
+
// rects manually.
|
|
2274
|
+
private _pointInOpenPopover(x: number, y: number): boolean {
|
|
2275
|
+
for (const el of document.querySelectorAll<HTMLElement>(":popover-open")) {
|
|
2276
|
+
const r = el.getBoundingClientRect();
|
|
2277
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2267
2284
|
private _handleDialogMousedown = (e: Event) => {
|
|
2268
2285
|
// Track whether the mousedown originated on the backdrop (the <dialog>
|
|
2269
2286
|
// itself). When the user drag-selects text inside the editor and the
|
|
2270
2287
|
// pointer overshoots to the backdrop, the subsequent click event fires
|
|
2271
2288
|
// with target === dialog. Without this guard, that click triggers
|
|
2272
2289
|
// requestClose() and the unsaved-changes confirmation pops up.
|
|
2273
|
-
|
|
2290
|
+
const me = e as MouseEvent;
|
|
2291
|
+
// Treat as backdrop only when target is the dialog AND the cursor is not
|
|
2292
|
+
// over an open popover (e.g. a toast notification in the top layer).
|
|
2293
|
+
this._mousedownOnBackdrop =
|
|
2294
|
+
e.target === this._dialogEl &&
|
|
2295
|
+
!this._pointInOpenPopover(me.clientX, me.clientY);
|
|
2296
|
+
this._mousedownPos = this._mousedownOnBackdrop
|
|
2297
|
+
? { x: me.clientX, y: me.clientY }
|
|
2298
|
+
: null;
|
|
2274
2299
|
};
|
|
2275
2300
|
|
|
2276
2301
|
private _handleDialogClick = (e: Event) => {
|
|
@@ -2279,6 +2304,26 @@ export class JantComposeDialog extends LitElement {
|
|
|
2279
2304
|
if (!this._mousedownOnBackdrop) return;
|
|
2280
2305
|
|
|
2281
2306
|
const mouseEvent = e as MouseEvent;
|
|
2307
|
+
|
|
2308
|
+
// If the pointer moved more than 4px since mousedown, the user was
|
|
2309
|
+
// dragging (e.g. selecting text in a toast on top of the backdrop) —
|
|
2310
|
+
// don't treat that as an intentional dismiss click.
|
|
2311
|
+
if (this._mousedownPos) {
|
|
2312
|
+
const dx = mouseEvent.clientX - this._mousedownPos.x;
|
|
2313
|
+
const dy = mouseEvent.clientY - this._mousedownPos.y;
|
|
2314
|
+
if (dx * dx + dy * dy > 16) return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// Also guard against text-selection drags that end back on the backdrop.
|
|
2318
|
+
const selection = document.getSelection();
|
|
2319
|
+
if (selection && !selection.isCollapsed) return;
|
|
2320
|
+
|
|
2321
|
+
// Guard against click pass-through from a top-layer popover: browsers can
|
|
2322
|
+
// route a click on a popover to the dialog backdrop simultaneously.
|
|
2323
|
+
if (this._pointInOpenPopover(mouseEvent.clientX, mouseEvent.clientY)) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2282
2327
|
const hitTarget = document.elementFromPoint(
|
|
2283
2328
|
mouseEvent.clientX,
|
|
2284
2329
|
mouseEvent.clientY,
|
|
@@ -2744,9 +2789,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
2744
2789
|
|
|
2745
2790
|
// Enter thread mode
|
|
2746
2791
|
this._threadItems = [
|
|
2747
|
-
{ id:
|
|
2792
|
+
{ id: randomUUID(), format: post.format },
|
|
2748
2793
|
...ordered.map((p) => ({
|
|
2749
|
-
id:
|
|
2794
|
+
id: randomUUID(),
|
|
2750
2795
|
format: p.format,
|
|
2751
2796
|
})),
|
|
2752
2797
|
];
|
|
@@ -3117,7 +3162,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3117
3162
|
if (draft.threadItems && draft.threadItems.length >= 2) {
|
|
3118
3163
|
this._format = draft.threadItems[0].format;
|
|
3119
3164
|
this._threadItems = draft.threadItems.map((item) => ({
|
|
3120
|
-
id:
|
|
3165
|
+
id: randomUUID(),
|
|
3121
3166
|
format: item.format,
|
|
3122
3167
|
}));
|
|
3123
3168
|
this._focusedThreadIndex = 0;
|
|
@@ -4962,6 +5007,38 @@ export class JantComposeDialog extends LitElement {
|
|
|
4962
5007
|
`;
|
|
4963
5008
|
}
|
|
4964
5009
|
|
|
5010
|
+
private _renderHideFromLatestQuickToggleRow() {
|
|
5011
|
+
if (this._visibilityLocked) return nothing;
|
|
5012
|
+
if (this._visibility === "private") return nothing;
|
|
5013
|
+
return html`
|
|
5014
|
+
<div class="compose-quick-actions-row">
|
|
5015
|
+
${this._renderHideFromLatestQuickToggle()}
|
|
5016
|
+
</div>
|
|
5017
|
+
`;
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
private _renderHideFromLatestQuickToggle() {
|
|
5021
|
+
if (this._visibilityLocked) return nothing;
|
|
5022
|
+
if (this._visibility === "private") return nothing;
|
|
5023
|
+
|
|
5024
|
+
const checked = this._visibility === "latest_hidden";
|
|
5025
|
+
return html`
|
|
5026
|
+
<label class="compose-publish-quick-toggle">
|
|
5027
|
+
<input
|
|
5028
|
+
type="checkbox"
|
|
5029
|
+
class="input compose-publish-quick-toggle-input"
|
|
5030
|
+
.checked=${checked}
|
|
5031
|
+
?disabled=${this._loading}
|
|
5032
|
+
@change=${(e: Event) => {
|
|
5033
|
+
const target = e.target as HTMLInputElement;
|
|
5034
|
+
this._setVisibility(target.checked ? "latest_hidden" : "public");
|
|
5035
|
+
}}
|
|
5036
|
+
/>
|
|
5037
|
+
<span>${this.labels.publishHideFromLatest}</span>
|
|
5038
|
+
</label>
|
|
5039
|
+
`;
|
|
5040
|
+
}
|
|
5041
|
+
|
|
4965
5042
|
private _renderEditLoadingState() {
|
|
4966
5043
|
return html`
|
|
4967
5044
|
<div
|
|
@@ -5021,8 +5098,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
5021
5098
|
const bodyJson = currentEditor?.getNormalizedBodyJson() ?? null;
|
|
5022
5099
|
|
|
5023
5100
|
this._threadItems = [
|
|
5024
|
-
{ id:
|
|
5025
|
-
{ id:
|
|
5101
|
+
{ id: randomUUID(), format: this._format },
|
|
5102
|
+
{ id: randomUUID(), format: lastFormat },
|
|
5026
5103
|
];
|
|
5027
5104
|
|
|
5028
5105
|
// Capture rating state before re-render (these can't change asynchronously)
|
|
@@ -5081,7 +5158,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5081
5158
|
} else {
|
|
5082
5159
|
this._threadItems = [
|
|
5083
5160
|
...this._threadItems,
|
|
5084
|
-
{ id:
|
|
5161
|
+
{ id: randomUUID(), format: lastFormat },
|
|
5085
5162
|
];
|
|
5086
5163
|
}
|
|
5087
5164
|
|
|
@@ -5343,14 +5420,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
5343
5420
|
${isOpeningEdit
|
|
5344
5421
|
? nothing
|
|
5345
5422
|
: html`<div
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5423
|
+
class=${classMap({
|
|
5424
|
+
"compose-action-row": true,
|
|
5425
|
+
"compose-action-row-overlay-open":
|
|
5426
|
+
this._showPublishPanel || this._showCollection,
|
|
5427
|
+
})}
|
|
5428
|
+
>
|
|
5429
|
+
${this._renderCollectionSelector()}
|
|
5430
|
+
${this._renderPublishButton()}
|
|
5431
|
+
</div>
|
|
5432
|
+
${this._renderHideFromLatestQuickToggleRow()}`}
|
|
5354
5433
|
${this._renderMobilePublishPanel()} ${this._renderAttachedPanel()}
|
|
5355
5434
|
${this._renderAltPanel()} ${this._renderDraftsPanel()}
|
|
5356
5435
|
${this._renderConfirmPanel()}
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
adoptPendingInlineImageUploads,
|
|
48
48
|
} from "../tiptap/inline-image-upload.js";
|
|
49
49
|
import { isSafeAbsoluteUrl } from "../../lib/url.js";
|
|
50
|
+
import { randomUUID } from "../random-uuid.js";
|
|
50
51
|
|
|
51
52
|
interface ComposeFilePickerCloseDetail {
|
|
52
53
|
cancelled: boolean;
|
|
@@ -220,7 +221,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
220
221
|
private _lastEditorSelection: ComposeEditorSelection | null = null;
|
|
221
222
|
private _emojiPickerEl: HTMLElement | null = null;
|
|
222
223
|
private _emojiContainer: HTMLElement | null = null;
|
|
223
|
-
private readonly _urlStatusId = `compose-url-status-${
|
|
224
|
+
private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
|
|
224
225
|
private _onDocClickBound = this._onDocumentClick.bind(this);
|
|
225
226
|
private _scrollBufferApplied = false;
|
|
226
227
|
private _filePickerCleanup: (() => void) | null = null;
|
|
@@ -880,7 +881,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
880
881
|
// Convert media attachments to ComposeAttachment[] with status "done"
|
|
881
882
|
if (data.media?.length) {
|
|
882
883
|
const attachments = data.media.map((m) => ({
|
|
883
|
-
clientId:
|
|
884
|
+
clientId: randomUUID(),
|
|
884
885
|
file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
|
|
885
886
|
previewUrl: m.previewUrl,
|
|
886
887
|
posterUrl: null,
|
|
@@ -906,7 +907,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
906
907
|
// Invalid JSON — leave as null
|
|
907
908
|
}
|
|
908
909
|
return {
|
|
909
|
-
clientId: t.clientId ??
|
|
910
|
+
clientId: t.clientId ?? randomUUID(),
|
|
910
911
|
bodyJson: parsed,
|
|
911
912
|
bodyHtml: t.bodyHtml ?? "",
|
|
912
913
|
summary: t.summary,
|
|
@@ -979,7 +980,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
979
980
|
|
|
980
981
|
private _openAttachedText() {
|
|
981
982
|
const item: AttachedTextItem = {
|
|
982
|
-
clientId:
|
|
983
|
+
clientId: randomUUID(),
|
|
983
984
|
bodyJson: null,
|
|
984
985
|
bodyHtml: "",
|
|
985
986
|
summary: "",
|
|
@@ -1242,7 +1243,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1242
1243
|
continue;
|
|
1243
1244
|
}
|
|
1244
1245
|
|
|
1245
|
-
const clientId =
|
|
1246
|
+
const clientId = randomUUID();
|
|
1246
1247
|
const previewUrl = URL.createObjectURL(file);
|
|
1247
1248
|
newAttachments.push({
|
|
1248
1249
|
clientId,
|
|
@@ -1961,6 +1962,11 @@ export class JantComposeEditor extends LitElement {
|
|
|
1961
1962
|
</svg>
|
|
1962
1963
|
<span class="compose-retry-label">${this.labels.retryAll}</span>
|
|
1963
1964
|
</span>
|
|
1965
|
+
${a.error
|
|
1966
|
+
? html`<span class="compose-attachment-error-msg"
|
|
1967
|
+
>${a.error}</span
|
|
1968
|
+
>`
|
|
1969
|
+
: nothing}
|
|
1964
1970
|
</button>
|
|
1965
1971
|
`
|
|
1966
1972
|
: nothing}
|
|
@@ -1976,7 +1982,6 @@ export class JantComposeEditor extends LitElement {
|
|
|
1976
1982
|
<button
|
|
1977
1983
|
type="button"
|
|
1978
1984
|
class="compose-attachment-remove"
|
|
1979
|
-
title=${this.labels.removeAttachment}
|
|
1980
1985
|
aria-label=${this.labels.removeAttachment}
|
|
1981
1986
|
@click=${onClick}
|
|
1982
1987
|
>
|
|
@@ -719,11 +719,29 @@ export class JantPostMenu extends LitElement {
|
|
|
719
719
|
const article = document.querySelector<HTMLElement>(
|
|
720
720
|
`article[data-post-id="${this._data.id}"]`,
|
|
721
721
|
);
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
722
|
+
// If the post is inside a thread group, only remove its .thread-item
|
|
723
|
+
// wrapper so sibling posts in the same thread stay visible. Fall back
|
|
724
|
+
// to removing the .feed-item wrapper (or the article itself) only when
|
|
725
|
+
// the post is standalone or the thread group is now empty.
|
|
726
|
+
const threadItem = article?.closest<HTMLElement>(".thread-item");
|
|
727
|
+
const threadGroup = threadItem?.closest<HTMLElement>(".thread-group");
|
|
728
|
+
if (threadItem && threadGroup) {
|
|
729
|
+
threadItem.remove();
|
|
730
|
+
const remainingPosts = threadGroup.querySelectorAll(
|
|
731
|
+
".thread-item:not(.thread-item-gap)",
|
|
732
|
+
);
|
|
733
|
+
if (remainingPosts.length === 0) {
|
|
734
|
+
const feedItem = threadGroup.closest<HTMLElement>(".feed-item");
|
|
735
|
+
const feedContainer = feedItem?.parentElement ?? null;
|
|
736
|
+
(feedItem ?? threadGroup).remove();
|
|
737
|
+
removeLeadingFeedDivider(feedContainer);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
const feedItem = article?.closest<HTMLElement>(".feed-item");
|
|
741
|
+
const feedContainer = feedItem?.parentElement ?? null;
|
|
742
|
+
(feedItem ?? article)?.remove();
|
|
743
|
+
removeLeadingFeedDivider(feedContainer);
|
|
744
|
+
}
|
|
727
745
|
|
|
728
746
|
showToast("Post deleted.");
|
|
729
747
|
} catch {
|
|
@@ -423,7 +423,8 @@ async function uploadFile(
|
|
|
423
423
|
} catch (error) {
|
|
424
424
|
const message = error instanceof Error ? error.message : "Upload failed";
|
|
425
425
|
editor?.updateAttachmentStatus(clientId, "error", null, message);
|
|
426
|
-
|
|
426
|
+
// Error is shown on the attachment thumbnail; only toast when there's no editor context.
|
|
427
|
+
if (!editor) showToast(message, "error");
|
|
427
428
|
return null;
|
|
428
429
|
}
|
|
429
430
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC4122 v4 UUID generator with fallback for insecure contexts.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.randomUUID()` is only available in secure contexts (HTTPS or
|
|
5
|
+
* localhost). Self-hosted Jant deployments are often accessed over plain
|
|
6
|
+
* HTTP on a LAN address, where the native API is `undefined`. This helper
|
|
7
|
+
* uses the native API when available and falls back to a `Math.random`-based
|
|
8
|
+
* v4 string otherwise. The fallback is not cryptographically strong, but
|
|
9
|
+
* client-side IDs here only need to be unique within a single page session.
|
|
10
|
+
*/
|
|
11
|
+
export const randomUUID = (): string => {
|
|
12
|
+
if (
|
|
13
|
+
typeof crypto !== "undefined" &&
|
|
14
|
+
typeof crypto.randomUUID === "function"
|
|
15
|
+
) {
|
|
16
|
+
return crypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
19
|
+
const r = (Math.random() * 16) | 0;
|
|
20
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
21
|
+
return v.toString(16);
|
|
22
|
+
});
|
|
23
|
+
};
|
package/src/client/toast.ts
CHANGED
|
@@ -22,10 +22,17 @@ export const QUEUED_TOAST_STORAGE_KEY = "jant.pendingToast";
|
|
|
22
22
|
|
|
23
23
|
/** Ensure the toast container is in the top layer (above <dialog> etc.) */
|
|
24
24
|
function ensureTopLayer(container: HTMLElement): void {
|
|
25
|
+
if (typeof container.showPopover !== "function") return;
|
|
26
|
+
|
|
27
|
+
// Re-promote above any modal dialog that was opened after the toast container.
|
|
25
28
|
if (
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
container.matches(":popover-open") &&
|
|
30
|
+
document.querySelector("dialog[open]")
|
|
28
31
|
) {
|
|
32
|
+
container.hidePopover();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!container.matches(":popover-open")) {
|
|
29
36
|
container.showPopover();
|
|
30
37
|
}
|
|
31
38
|
}
|
|
@@ -85,6 +92,11 @@ const TOAST_ICONS = {
|
|
|
85
92
|
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>',
|
|
86
93
|
};
|
|
87
94
|
|
|
95
|
+
const COPY_ICON =
|
|
96
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
97
|
+
const CHECK_ICON =
|
|
98
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>';
|
|
99
|
+
|
|
88
100
|
/** Build toast inner content using safe DOM APIs (icon is trusted, text uses textContent). */
|
|
89
101
|
function setToastContent(
|
|
90
102
|
toast: HTMLElement,
|
|
@@ -103,6 +115,21 @@ function setToastContent(
|
|
|
103
115
|
a.textContent = action.label;
|
|
104
116
|
toast.appendChild(a);
|
|
105
117
|
}
|
|
118
|
+
if (type === "error" && navigator.clipboard) {
|
|
119
|
+
const btn = document.createElement("button");
|
|
120
|
+
btn.className = "toast-copy";
|
|
121
|
+
btn.setAttribute("aria-label", "Copy error message");
|
|
122
|
+
btn.innerHTML = COPY_ICON;
|
|
123
|
+
btn.addEventListener("click", () => {
|
|
124
|
+
navigator.clipboard.writeText(message).then(() => {
|
|
125
|
+
btn.innerHTML = CHECK_ICON;
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
btn.innerHTML = COPY_ICON;
|
|
128
|
+
}, 1500);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
toast.appendChild(btn);
|
|
132
|
+
}
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
/**
|
|
@@ -108,7 +108,7 @@ async function initiateUpload(file: File): Promise<InitiateResponse> {
|
|
|
108
108
|
headers: { "Content-Type": "application/json" },
|
|
109
109
|
body: JSON.stringify({
|
|
110
110
|
filename: file.name,
|
|
111
|
-
contentType: file.type,
|
|
111
|
+
contentType: file.type || "application/octet-stream",
|
|
112
112
|
size: file.size,
|
|
113
113
|
checksumSha256: await sha256Base64(file),
|
|
114
114
|
}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DROP TABLE `sync_job`;
|