@jant/core 0.3.43 → 0.3.45

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 (77) hide show
  1. package/dist/{app-GbfwoeDJ.js → app-C-L7wL6o.js} +485 -452
  2. package/dist/app-Hvqe7Ks_.js +5 -0
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/client-DDs6NzB3.css +2 -0
  5. package/dist/client/_assets/{client-auth-CXILhW1b.js → client-auth-Dcon89Av.js} +30 -11
  6. package/dist/client/_assets/{client-D95FNDg5.js → client-dSfWfMe9.js} +7 -7
  7. package/dist/{github-sync-7y_nTXx1.js → github-sync-CQ1x271f.js} +3 -0
  8. package/dist/index.js +4 -87
  9. package/dist/node.js +3 -3
  10. package/package.json +1 -1
  11. package/src/client/components/jant-compose-dialog.ts +87 -9
  12. package/src/client/components/jant-compose-editor.ts +5 -1
  13. package/src/client/components/jant-post-menu.ts +23 -5
  14. package/src/client/compose-bridge.ts +2 -1
  15. package/src/client/toast.ts +29 -2
  16. package/src/client/upload-session.ts +1 -1
  17. package/src/db/migrations/0019_bored_magus.sql +2 -0
  18. package/src/db/migrations/0020_free_zaladane.sql +1 -0
  19. package/src/db/migrations/meta/0019_snapshot.json +2238 -0
  20. package/src/db/migrations/meta/0020_snapshot.json +2129 -0
  21. package/src/db/migrations/meta/_journal.json +14 -0
  22. package/src/db/migrations/pg/0017_bright_beyonder.sql +2 -0
  23. package/src/db/migrations/pg/0018_red_warlock.sql +1 -0
  24. package/src/db/migrations/pg/meta/0017_snapshot.json +2862 -0
  25. package/src/db/migrations/pg/meta/0018_snapshot.json +2739 -0
  26. package/src/db/migrations/pg/meta/_journal.json +14 -0
  27. package/src/db/pg/schema.ts +4 -30
  28. package/src/db/schema.ts +4 -39
  29. package/src/i18n/locales/public/en.po +10 -5
  30. package/src/i18n/locales/public/en.ts +1 -1
  31. package/src/i18n/locales/public/zh-Hans.po +10 -5
  32. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  33. package/src/i18n/locales/public/zh-Hant.po +10 -5
  34. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  35. package/src/index.ts +0 -3
  36. package/src/lib/__tests__/resolve-config.test.ts +4 -4
  37. package/src/lib/__tests__/startup-config.test.ts +27 -2
  38. package/src/lib/constants.ts +1 -0
  39. package/src/lib/github-sync-trigger.ts +7 -51
  40. package/src/lib/icons.ts +37 -0
  41. package/src/lib/startup-config.ts +53 -6
  42. package/src/routes/api/github-sync.tsx +36 -14
  43. package/src/routes/api/internal/sites.ts +1 -0
  44. package/src/routes/pages/home.tsx +2 -0
  45. package/src/routes/pages/latest.tsx +2 -0
  46. package/src/runtime/__tests__/readiness.test.ts +34 -0
  47. package/src/runtime/readiness.ts +8 -4
  48. package/src/services/__tests__/collection.test.ts +13 -11
  49. package/src/services/__tests__/site-admin.test.ts +85 -0
  50. package/src/services/github-sync.ts +6 -0
  51. package/src/services/site-admin.ts +66 -1
  52. package/src/styles/components.css +14 -0
  53. package/src/styles/ui.css +109 -0
  54. package/src/types/bindings.ts +0 -2
  55. package/src/types/config.ts +1 -1
  56. package/src/types/props.ts +2 -0
  57. package/src/ui/__tests__/font-themes.test.ts +2 -2
  58. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -17
  59. package/src/ui/feed/LinkCard.tsx +3 -20
  60. package/src/ui/feed/LinkPreview.tsx +5 -19
  61. package/src/ui/feed/PostStatusBadges.tsx +4 -38
  62. package/src/ui/font-themes.ts +17 -17
  63. package/src/ui/layouts/BaseLayout.tsx +14 -29
  64. package/src/ui/pages/HomePage.tsx +21 -5
  65. package/src/ui/shared/DecorativeQuoteMark.tsx +2 -13
  66. package/src/ui/shared/Icon.tsx +60 -0
  67. package/src/ui/shared/IconSprite.tsx +57 -0
  68. package/src/ui/shared/PostFooter.tsx +6 -62
  69. package/src/ui/shared/custom-icons.ts +132 -0
  70. package/src/ui/shared/icon-collector.ts +37 -0
  71. package/dist/app-Ctl0T0zO.js +0 -5
  72. package/dist/client/_assets/client-C_kImWZj.css +0 -2
  73. package/src/lib/github-sync-queue-handler.ts +0 -69
  74. package/src/lib/github-sync-worker.ts +0 -72
  75. package/src/lib/job-queue-cf.ts +0 -18
  76. package/src/lib/job-queue-db.ts +0 -149
  77. package/src/lib/job-queue.ts +0 -35
@@ -641,6 +641,7 @@ export class JantComposeDialog extends LitElement {
641
641
  private _suppressBeforeUnload = false;
642
642
  private _dialogEl: HTMLDialogElement | null = null;
643
643
  private _mousedownOnBackdrop = false;
644
+ private _mousedownPos: { x: number; y: number } | null = null;
644
645
  private _filePickerActive = false;
645
646
  private _ignoreNextEscapeClose = false;
646
647
  private _openEditRequestId = 0;
@@ -2264,13 +2265,36 @@ export class JantComposeDialog extends LitElement {
2264
2265
  this.requestClose();
2265
2266
  };
2266
2267
 
2268
+ // Returns true if the given point is inside any open top-layer popover.
2269
+ // Browsers sometimes fire backdrop click events even when the pointer is
2270
+ // over a popover that is rendered above the dialog in the top layer —
2271
+ // document.elementFromPoint() ignores the top layer, so we check bounding
2272
+ // rects manually.
2273
+ private _pointInOpenPopover(x: number, y: number): boolean {
2274
+ for (const el of document.querySelectorAll<HTMLElement>(":popover-open")) {
2275
+ const r = el.getBoundingClientRect();
2276
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
2277
+ return true;
2278
+ }
2279
+ }
2280
+ return false;
2281
+ }
2282
+
2267
2283
  private _handleDialogMousedown = (e: Event) => {
2268
2284
  // Track whether the mousedown originated on the backdrop (the <dialog>
2269
2285
  // itself). When the user drag-selects text inside the editor and the
2270
2286
  // pointer overshoots to the backdrop, the subsequent click event fires
2271
2287
  // with target === dialog. Without this guard, that click triggers
2272
2288
  // requestClose() and the unsaved-changes confirmation pops up.
2273
- this._mousedownOnBackdrop = e.target === this._dialogEl;
2289
+ const me = e as MouseEvent;
2290
+ // Treat as backdrop only when target is the dialog AND the cursor is not
2291
+ // over an open popover (e.g. a toast notification in the top layer).
2292
+ this._mousedownOnBackdrop =
2293
+ e.target === this._dialogEl &&
2294
+ !this._pointInOpenPopover(me.clientX, me.clientY);
2295
+ this._mousedownPos = this._mousedownOnBackdrop
2296
+ ? { x: me.clientX, y: me.clientY }
2297
+ : null;
2274
2298
  };
2275
2299
 
2276
2300
  private _handleDialogClick = (e: Event) => {
@@ -2279,6 +2303,26 @@ export class JantComposeDialog extends LitElement {
2279
2303
  if (!this._mousedownOnBackdrop) return;
2280
2304
 
2281
2305
  const mouseEvent = e as MouseEvent;
2306
+
2307
+ // If the pointer moved more than 4px since mousedown, the user was
2308
+ // dragging (e.g. selecting text in a toast on top of the backdrop) —
2309
+ // don't treat that as an intentional dismiss click.
2310
+ if (this._mousedownPos) {
2311
+ const dx = mouseEvent.clientX - this._mousedownPos.x;
2312
+ const dy = mouseEvent.clientY - this._mousedownPos.y;
2313
+ if (dx * dx + dy * dy > 16) return;
2314
+ }
2315
+
2316
+ // Also guard against text-selection drags that end back on the backdrop.
2317
+ const selection = document.getSelection();
2318
+ if (selection && !selection.isCollapsed) return;
2319
+
2320
+ // Guard against click pass-through from a top-layer popover: browsers can
2321
+ // route a click on a popover to the dialog backdrop simultaneously.
2322
+ if (this._pointInOpenPopover(mouseEvent.clientX, mouseEvent.clientY)) {
2323
+ return;
2324
+ }
2325
+
2282
2326
  const hitTarget = document.elementFromPoint(
2283
2327
  mouseEvent.clientX,
2284
2328
  mouseEvent.clientY,
@@ -4962,6 +5006,38 @@ export class JantComposeDialog extends LitElement {
4962
5006
  `;
4963
5007
  }
4964
5008
 
5009
+ private _renderHideFromLatestQuickToggleRow() {
5010
+ if (this._visibilityLocked) return nothing;
5011
+ if (this._visibility === "private") return nothing;
5012
+ return html`
5013
+ <div class="compose-quick-actions-row">
5014
+ ${this._renderHideFromLatestQuickToggle()}
5015
+ </div>
5016
+ `;
5017
+ }
5018
+
5019
+ private _renderHideFromLatestQuickToggle() {
5020
+ if (this._visibilityLocked) return nothing;
5021
+ if (this._visibility === "private") return nothing;
5022
+
5023
+ const checked = this._visibility === "latest_hidden";
5024
+ return html`
5025
+ <label class="compose-publish-quick-toggle">
5026
+ <input
5027
+ type="checkbox"
5028
+ class="input compose-publish-quick-toggle-input"
5029
+ .checked=${checked}
5030
+ ?disabled=${this._loading}
5031
+ @change=${(e: Event) => {
5032
+ const target = e.target as HTMLInputElement;
5033
+ this._setVisibility(target.checked ? "latest_hidden" : "public");
5034
+ }}
5035
+ />
5036
+ <span>${this.labels.publishHideFromLatest}</span>
5037
+ </label>
5038
+ `;
5039
+ }
5040
+
4965
5041
  private _renderEditLoadingState() {
4966
5042
  return html`
4967
5043
  <div
@@ -5343,14 +5419,16 @@ export class JantComposeDialog extends LitElement {
5343
5419
  ${isOpeningEdit
5344
5420
  ? nothing
5345
5421
  : html`<div
5346
- class=${classMap({
5347
- "compose-action-row": true,
5348
- "compose-action-row-overlay-open":
5349
- this._showPublishPanel || this._showCollection,
5350
- })}
5351
- >
5352
- ${this._renderCollectionSelector()} ${this._renderPublishButton()}
5353
- </div>`}
5422
+ class=${classMap({
5423
+ "compose-action-row": true,
5424
+ "compose-action-row-overlay-open":
5425
+ this._showPublishPanel || this._showCollection,
5426
+ })}
5427
+ >
5428
+ ${this._renderCollectionSelector()}
5429
+ ${this._renderPublishButton()}
5430
+ </div>
5431
+ ${this._renderHideFromLatestQuickToggleRow()}`}
5354
5432
  ${this._renderMobilePublishPanel()} ${this._renderAttachedPanel()}
5355
5433
  ${this._renderAltPanel()} ${this._renderDraftsPanel()}
5356
5434
  ${this._renderConfirmPanel()}
@@ -1961,6 +1961,11 @@ export class JantComposeEditor extends LitElement {
1961
1961
  </svg>
1962
1962
  <span class="compose-retry-label">${this.labels.retryAll}</span>
1963
1963
  </span>
1964
+ ${a.error
1965
+ ? html`<span class="compose-attachment-error-msg"
1966
+ >${a.error}</span
1967
+ >`
1968
+ : nothing}
1964
1969
  </button>
1965
1970
  `
1966
1971
  : nothing}
@@ -1976,7 +1981,6 @@ export class JantComposeEditor extends LitElement {
1976
1981
  <button
1977
1982
  type="button"
1978
1983
  class="compose-attachment-remove"
1979
- title=${this.labels.removeAttachment}
1980
1984
  aria-label=${this.labels.removeAttachment}
1981
1985
  @click=${onClick}
1982
1986
  >
@@ -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
- // Remove the feed item wrapper if it exists, otherwise the article itself
723
- const feedItem = article?.closest(".feed-item");
724
- const feedContainer = feedItem?.parentElement;
725
- (feedItem ?? article)?.remove();
726
- removeLeadingFeedDivider(feedContainer);
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
- showToast(message, "error");
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
  }
@@ -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
- !container.matches(":popover-open") &&
27
- typeof container.showPopover === "function"
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,2 @@
1
+ ALTER TABLE `site` ADD `provisioning_idempotency_key` text;--> statement-breakpoint
2
+ CREATE UNIQUE INDEX `uq_site_provisioning_idempotency_key` ON `site` (`provisioning_idempotency_key`) WHERE "site"."provisioning_idempotency_key" IS NOT NULL;
@@ -0,0 +1 @@
1
+ DROP TABLE `sync_job`;