@chance722/dsh-inbox 0.2.3 → 0.2.5

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/lib/client.js CHANGED
@@ -809,6 +809,11 @@ window.__ModuleLoader__.load({
809
809
  "settings.region": "\u533A\u57DF",
810
810
  "settings.bucketAddress": "\u5730\u5740",
811
811
  "settings.bucketDir": "\u76EE\u5F55",
812
+ "settings.adoptForeign": "\u540C\u65F6\u5408\u5E76\u522B\u7684\u540C\u6B65\u76EE\u5F55",
813
+ "settings.adoptForeignHint": "\uFF08\u6362\u8FC7\u76EE\u5F55\u7684\u673A\u5668\u7559\u5728\u4E91\u7AEF\u7684\u8BB0\u5F55\u4E5F\u4F1A\u62C9\u56DE\u6765\uFF1B\u9ED8\u8BA4\u5173\uFF09",
814
+ // The default is the thing people get wrong: blank and `/` are the same as
815
+ // `inbox`, and the sync tree is `<directory>/sync`.
816
+ "settings.directoryPlaceholder": "inbox\uFF08\u9ED8\u8BA4\uFF1B\u7559\u7A7A\u6216 / \u90FD\u7B49\u4E8E\u5B83\uFF09",
812
817
  "settings.dirS3.lead": "\u76EE\u5F55\u90A3\u4E00\u680F\u540C\u65F6\u662F S3 \u7684 key \u524D\u7F00\uFF08\u9ED8\u8BA4 ",
813
818
  "settings.dirS3.mid": "\uFF0C\u4F1A\u8F6C\u6210 ",
814
819
  "settings.dirS3.tail": "\uFF09\u3002",
@@ -858,17 +863,26 @@ window.__ModuleLoader__.load({
858
863
  "sync.pushUnconfigured": "\u63A8\u9001\uFF1A\u8FD8\u6CA1\u914D\u7F6E\u8FDC\u7AEF",
859
864
  "sync.pushFailed": "\u63A8\u9001\u5931\u8D25\uFF1A{reason}",
860
865
  "sync.pushUnknown": "\u672A\u77E5\u539F\u56E0",
861
- "sync.pushUpToDate": "\u63A8\u9001\uFF1A\u6CA1\u6709\u65B0\u5185\u5BB9\uFF08{count} \u6761\u5DF2\u662F\u6700\u65B0\uFF09",
862
- "sync.pushDone": "\u63A8\u9001\uFF1A{records} \u6761\u8BB0\u5F55 / {attachments} \u4E2A\u9644\u4EF6",
863
866
  "sync.pushPartial": "{head} \xB7 \u90E8\u5206\u5931\u8D25\uFF1A{reason}",
864
867
  "sync.pullNoAddress": "\u8FD8\u6CA1\u914D\u7F6E\u5730\u5740",
865
868
  "sync.pullFailed": "\u62C9\u53D6\u5931\u8D25\uFF1A{reason}",
866
- "sync.pullNothingNew": "\u4E91\u7AEF\u7684\u8BB0\u5F55\u6CA1\u6709\u65B0\u7684",
867
- "sync.pullMerged": "\u4ECE\u4E91\u7AEF\u5408\u5E76 {count} \u6761{attachments}",
868
- "sync.pullAttachments": " / {count} \u4E2A\u9644\u4EF6",
869
- "sync.pullDone": "\u62C9\u53D6\u5B8C\u6210\uFF1A\u8FDC\u7AEF\u5217\u51FA {listed} \u9879\uFF0C\u65B0\u5165\u5E93 {pulled} \u6761\uFF0C\u8DF3\u8FC7 {skipped} \u6761{tail}",
870
- "sync.pullSkipWhy": "\uFF08\u81EA\u5DF1\u7684\u540C\u6B65\u5BF9\u8C61 {sync} \u9879\u3001\u6BD4\u4E0A\u6B21\u62C9\u53D6\u66F4\u65E7 {older} \u9879\uFF09",
871
- "sync.pullForeignSync": " \xB7 \u26A0\uFE0F \u8FD8\u53D1\u73B0\u53E6\u4E00\u5957\u540C\u6B65\u524D\u7F00 {roots}\uFF08\u672C\u673A\u662F {ours}\uFF09\uFF1A\u4E24\u53F0\u673A\u5668\u7684\u300C\u76EE\u5F55\u300D\u8BBE\u7F6E\u4E0D\u4E00\u81F4\uFF0C\u5148\u628A\u4E24\u8FB9\u6539\u6210\u540C\u4E00\u4E2A",
869
+ // The refresh button's tooltip: counts first, reasons only when something was
870
+ // skipped. The toast carries the two numbers alone.
871
+ // No number here on purpose: the push considers every row it has, tombstones
872
+ // included, so "26 条已是最新" next to a header saying "共 13 条" is a
873
+ // mismatch the reader has to resolve for themselves (2026-09-21).
874
+ "sync.detailPushedIdle": "\u63A8\u9001 0 \u6761\uFF08\u6CA1\u6709\u65B0\u6539\u52A8\uFF09",
875
+ "sync.detailPushed": "\u63A8\u9001 {records} \u6761 / {attachments} \u4E2A\u9644\u4EF6",
876
+ "sync.detailPulled": "\u62C9\u53D6 {count} \u6761 \xB7 \u4E91\u7AEF {records} \u6761\u8BB0\u5F55 / {files} \u4E2A\u9644\u4EF6",
877
+ "sync.detailAdded": "\uFF08\u5176\u4E2D {count} \u6761\u662F\u65B0\u8BB0\u5F55\uFF09",
878
+ "sync.detailDeleted": "\uFF08\u5176\u4E2D {count} \u6761\u662F\u5220\u9664\uFF0C\u4F1A\u8FDB\u56DE\u6536\u7AD9\uFF09",
879
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F \u53E6\u6709 {records} \u6761\u8BB0\u5F55\u5728\u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}",
880
+ "sync.detailForeign": " \xB7 \u26A0\uFE0F \u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}\uFF08{records} \u6761\u8BB0\u5F55\uFF1B\u672C\u673A {ours}\uFF09",
881
+ // The toast: two numbers, nothing else. Everything else about a sync is
882
+ // detail, and detail belongs in a line that does not vanish.
883
+ "sync.shortPushed": "\u63A8\u9001 {count} \u6761",
884
+ "sync.shortPulled": "\u62C9\u53D6 {count} \u6761",
885
+ "sync.shortIdle": "\u5DF2\u662F\u6700\u65B0",
872
886
  "sync.pullFailures": " \xB7 \u5931\u8D25 {count}\uFF1A{reason}",
873
887
  "sync.thisPage": " \xB7 \u672C\u9875 {count} \u6761",
874
888
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
@@ -938,10 +952,15 @@ window.__ModuleLoader__.load({
938
952
  "manual.5.auto.body": "\u5165\u5E93/\u6539\u52A8\u540E\u51E0\u79D2\u81EA\u52A8\u63A8\u9001\u4E00\u6B21\uFF08\u9632\u6296\uFF0C\u8FDE\u7740\u5B58\u4E94\u6761\u53EA\u4F1A\u63A8\u4E00\u6B21\uFF09\u3002",
939
953
  "manual.5.manual.label": "\u624B\u52A8",
940
954
  "manual.5.manual.body": "\u53F3\u4E0A\u89D2\u300C\u5237\u65B0\u300D= \u4E00\u6B21\u5B8C\u6574\u540C\u6B65\uFF1A\u5148\u63A8\u672C\u673A\u6539\u52A8\uFF0C\u518D\u62C9\u522B\u4EBA\u7684\uFF0C\u7136\u540E\u91CD\u8BFB\u5217\u8868\u3002",
955
+ // The one rule that decides whether two machines see each other at all.
956
+ "manual.5.twoMachines.label": "\u4E24\u53F0\u673A\u5668",
957
+ "manual.5.twoMachines.body": "\u4E24\u8FB9\u7684\u300C\u76EE\u5F55\u300D\u8981\u4E00\u6837\uFF08\u7559\u7A7A\u3001/\u3001inbox \u662F\u540C\u4E00\u4E2A\u610F\u601D\uFF09\u3002\u4E0D\u4E00\u6837\u65F6\u9ED8\u8BA4\u4E0D\u4E92\u901A\uFF1A\u5237\u65B0\u4F1A\u63D0\u793A\u300C\u53E6\u6709 N \u6761\u8BB0\u5F55\u5728\u522B\u7684\u540C\u6B65\u76EE\u5F55\u300D\u2014\u2014\u628A\u5B83\u4EEC\u6539\u6210\u540C\u4E00\u4E2A\uFF0C\u6216\u8005\u6253\u5F00\u8BBE\u7F6E\u91CC\u7684\u300C\u540C\u65F6\u5408\u5E76\u522B\u7684\u540C\u6B65\u76EE\u5F55\u300D\uFF0C\u90FD\u80FD\u5408\u8FC7\u6765\u3002",
941
958
  "manual.5.delete.label": "\u5220\u9664",
942
959
  "manual.5.delete.body": "\u9762\u677F\u91CC\u7684\u300C\u5220\u9664\u300D\u53EA\u662F\u628A\u5B83\u653E\u8FDB\u56DE\u6536\u7AD9\uFF08\u522B\u7684\u8BBE\u5907\u4E5F\u4F1A\u77E5\u9053\u5B83\u88AB\u5220\u4E86\uFF0C\u4E0D\u4F1A\u53C8\u88AB\u62C9\u56DE\u6765\uFF09\uFF1B\u300C\u6E05\u7A7A\u56DE\u6536\u7AD9\u300D\u624D\u662F\u771F\u5220\uFF0C\u4E91\u7AEF\u90A3\u4EFD\u4E5F\u4F1A\u4E00\u8D77\u5220\u3002",
943
- "manual.5.conflict.label": "\u51B2\u7A81",
944
- "manual.5.conflict.body": "\u591A\u8BBE\u5907\u6539\u540C\u4E00\u6761\u6309\u65F6\u95F4\u540E\u5199\u8D62\uFF0C\u4E0D\u7559\u51B2\u7A81\u526F\u672C\u3002\u4E0A\u4E91\u7684\u5BC6\u6587\u53EA\u6709\u8D26\u5BC6\u6B63\u6587\uFF0C\u5176\u4F59\u662F\u660E\u6587 \u2014\u2014 \u6876\u52A1\u5FC5\u8981\u6709\u8BBF\u95EE\u63A7\u5236\u3002",
960
+ "manual.5.merge.label": "\u5408\u5E76",
961
+ "manual.5.merge.body": "\u62C9\u56DE\u6765\u6309\u8BB0\u5F55\u7684 id + \u66F4\u65B0\u65F6\u95F4\u5408\u5E76\uFF1A\u8C01\u65B0\u8C01\u8D62\uFF0C\u4E0D\u7559\u51B2\u7A81\u526F\u672C\u3002",
962
+ "manual.5.cloud.label": "\u4E0A\u4E91\u4EC0\u4E48",
963
+ "manual.5.cloud.body": "\u8BB0\u5F55\u4E0E\u9644\u4EF6\u4E00\u8D77\u8D70\uFF1B\u53EA\u6709\u8D26\u5BC6\u6B63\u6587\u662F\u5BC6\u6587\uFF0C\u5176\u4F59\uFF08\u6587\u672C\u3001\u94FE\u63A5\u3001\u5907\u6CE8\u3001\u9644\u4EF6\uFF09\u662F\u660E\u6587\u2014\u2014\u6876\u52A1\u5FC5\u8BBE\u8BBF\u95EE\u63A7\u5236\u3002",
945
964
  "manual.6.title": "6. \u5728\u5BF9\u8BDD\u91CC\u53D6\u56DE\u6765",
946
965
  "manual.6.query.label": "\u67E5",
947
966
  "manual.6.query.body": "\u5728\u5BF9\u8BDD\u91CC\u76F4\u63A5\u95EE\uFF1A\u300C\u6211\u7684\u6536\u4EF6\u7BB1\u91CC\u6709\u54EA\u4E9B\u8FD8\u6CA1\u770B\u7684\u94FE\u63A5\uFF1F\u300D\u52A9\u624B\u4F1A\u53BB\u4ED3\u5E93\u91CC\u627E\uFF0C \u6700\u591A\u5217 10 \u6761\uFF0C\u5E76\u544A\u8BC9\u4F60\u8FD8\u5269\u51E0\u6761\u3002\u53EB\u5B83\u300C\u6536\u4EF6\u7BB1\u300D\u300C\u4ED3\u5E93\u300D\u300C\u4E2A\u4EBA\u4ED3\u5E93\u300D\u8FD8\u662F\u300Cinbox\u300D\u90FD\u8BA4\u3002",
@@ -1068,6 +1087,9 @@ window.__ModuleLoader__.load({
1068
1087
  "settings.region": "Region",
1069
1088
  "settings.bucketAddress": "Address",
1070
1089
  "settings.bucketDir": "Directory",
1090
+ "settings.adoptForeign": "Merge other sync directories too",
1091
+ "settings.adoptForeignHint": "(brings back records a machine left under an older directory; off by default)",
1092
+ "settings.directoryPlaceholder": "inbox (the default \u2014 blank or / means the same)",
1071
1093
  "settings.dirS3.lead": "the directory is also the key prefix for S3 (default ",
1072
1094
  "settings.dirS3.mid": ", which becomes ",
1073
1095
  "settings.dirS3.tail": ").",
@@ -1115,17 +1137,19 @@ window.__ModuleLoader__.load({
1115
1137
  "sync.pushUnconfigured": "Push: no remote configured yet",
1116
1138
  "sync.pushFailed": "Push failed: {reason}",
1117
1139
  "sync.pushUnknown": "unknown reason",
1118
- "sync.pushUpToDate": "Push: nothing new ({count} already current)",
1119
- "sync.pushDone": "Push: {records} records / {attachments} attachment(s)",
1120
1140
  "sync.pushPartial": "{head} \xB7 partly failed: {reason}",
1121
1141
  "sync.pullNoAddress": "No address configured yet",
1122
1142
  "sync.pullFailed": "Pull failed: {reason}",
1123
- "sync.pullNothingNew": "Nothing new in the cloud",
1124
- "sync.pullMerged": "Merged {count} from the cloud{attachments}",
1125
- "sync.pullAttachments": " / {count} attachment(s)",
1126
- "sync.pullDone": "Pull done: {listed} listed remotely, {pulled} filed, {skipped} skipped{tail}",
1127
- "sync.pullSkipWhy": " ({sync} of our own sync objects, {older} older than the last pull)",
1128
- "sync.pullForeignSync": " \xB7 \u26A0\uFE0F another sync prefix exists: {roots} (this machine: {ours}) \u2014 two machines have different directory settings; make them match",
1143
+ "sync.detailPushedIdle": "pushed 0 (nothing new)",
1144
+ "sync.detailPushed": "pushed {records} / {attachments} attachments",
1145
+ "sync.detailPulled": "pulled {count} \xB7 the cloud holds {records} records / {files} attachments",
1146
+ "sync.detailAdded": " ({count} of them new)",
1147
+ "sync.detailDeleted": " ({count} of them deletions \u2014 they land in the recycle bin)",
1148
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F {records} records live in another sync directory: {roots}",
1149
+ "sync.detailForeign": " \xB7 \u26A0\uFE0F another sync directory: {roots} ({records} records; ours is {ours})",
1150
+ "sync.shortPushed": "pushed {count}",
1151
+ "sync.shortPulled": "pulled {count}",
1152
+ "sync.shortIdle": "up to date",
1129
1153
  "sync.pullFailures": " \xB7 {count} failed: {reason}",
1130
1154
  "sync.thisPage": " \xB7 {count} on this page",
1131
1155
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
@@ -1192,10 +1216,14 @@ window.__ModuleLoader__.load({
1192
1216
  "manual.5.auto.body": "a few seconds after a capture or an edit a push goes out (debounced \u2014 five quick saves push once).",
1193
1217
  "manual.5.manual.label": "Manual",
1194
1218
  "manual.5.manual.body": "\u201CRefresh\u201D in the top right is a full sync: push local changes, pull the others, then re-read the list.",
1219
+ "manual.5.twoMachines.label": "Two machines",
1220
+ "manual.5.twoMachines.body": "Both machines need the same \u300C\u76EE\u5F55\u300D (blank, / and inbox all mean the same directory). While they differ nothing crosses over by default: the refresh says \u201CN records live in another sync directory\u201D, and either matching them or ticking \u201CMerge other sync directories too\u201D brings them in.",
1195
1221
  "manual.5.delete.label": "Deleting",
1196
1222
  "manual.5.delete.body": "\u201CDelete\u201D in the panel only moves a record to the bin (other devices learn it is gone instead of pushing it back); emptying the bin is the real delete, and the cloud copy goes with it.",
1197
- "manual.5.conflict.label": "Conflicts",
1198
- "manual.5.conflict.body": "two devices editing one record: the later write wins, no conflict copies. Only credential bodies travel encrypted \u2014 everything else is plain text up there, so the bucket needs access control.",
1223
+ "manual.5.merge.label": "Merging",
1224
+ "manual.5.merge.body": "Incoming records merge by id + timestamp: the newer write wins, no conflict copies.",
1225
+ "manual.5.cloud.label": "What goes up",
1226
+ "manual.5.cloud.body": "Records and attachments both travel; only credential bodies are ciphertext \u2014 text, links, notes and attachments are plain text, so the bucket needs access control.",
1199
1227
  "manual.6.title": "6. Getting things back in the conversation",
1200
1228
  "manual.6.query.label": "Ask",
1201
1229
  "manual.6.query.body": "just ask: \u201Cwhich links in my inbox haven\u2019t I read yet?\u201D The assistant searches the vault, lists at most 10, and tells you how many are left. \u6536\u4EF6\u7BB1 / \u4ED3\u5E93 / inbox all mean the same shelf.",
@@ -1758,7 +1786,12 @@ window.__ModuleLoader__.load({
1758
1786
  manual ran off the bottom of the screen with no visible frame.
1759
1787
  */
1760
1788
  width: "min(560px, 100%)",
1761
- maxHeight: "92vh",
1789
+ /*
1790
+ 80vh rather than 92vh: at 92 the card still felt like a full screen
1791
+ and it pushed the panel out of sight, while the point of the manual
1792
+ is to be readable *beside* what it explains (asked 2026-09-21).
1793
+ */
1794
+ maxHeight: "80vh",
1762
1795
  // Border inside the width and the cap: without it the card is 2px
1763
1796
  // wider than the number and 2px taller than the overlay's room.
1764
1797
  boxSizing: "border-box",
@@ -1809,6 +1842,7 @@ window.__ModuleLoader__.load({
1809
1842
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1810
1843
  "div",
1811
1844
  {
1845
+ className: "dsh-inbox-scroll",
1812
1846
  style: {
1813
1847
  padding: 18,
1814
1848
  display: "flex",
@@ -1852,8 +1886,10 @@ window.__ModuleLoader__.load({
1852
1886
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Section, { title: t("manual.5.title"), children: [
1853
1887
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.auto.label"), children: t("manual.5.auto.body") }),
1854
1888
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.manual.label"), children: t("manual.5.manual.body") }),
1855
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.delete.label"), children: t("manual.5.delete.body") }),
1856
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.conflict.label"), children: t("manual.5.conflict.body") })
1889
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.merge.label"), children: t("manual.5.merge.body") }),
1890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.twoMachines.label"), children: t("manual.5.twoMachines.body") }),
1891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.cloud.label"), children: t("manual.5.cloud.body") }),
1892
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.delete.label"), children: t("manual.5.delete.body") })
1857
1893
  ] }),
1858
1894
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Section, { title: t("manual.6.title"), children: [
1859
1895
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.6.query.label"), children: t("manual.6.query.body") }),
@@ -2098,6 +2134,7 @@ window.__ModuleLoader__.load({
2098
2134
  const [text, setText] = import_react8.default.useState("");
2099
2135
  const [staged, setStaged] = import_react8.default.useState([]);
2100
2136
  const [notice, setNotice] = import_react8.default.useState();
2137
+ const [syncDetail, setSyncDetail] = import_react8.default.useState();
2101
2138
  const [busy, setBusy] = import_react8.default.useState(false);
2102
2139
  const [dragging, setDragging] = import_react8.default.useState(false);
2103
2140
  const picker = import_react8.default.useRef(null);
@@ -2250,19 +2287,27 @@ window.__ModuleLoader__.load({
2250
2287
  setNotice(t("notice.syncing"));
2251
2288
  try {
2252
2289
  const pushed = await call(INBOX_ENDPOINT_PUSH, {});
2253
- const pushLine = pushed.ok ? describePush(pushed.value) : t("notice.pushFailed", { reason: pushed.error.message });
2254
2290
  const pulled = await call(INBOX_ENDPOINT_PULL, {});
2255
- let suffix = "";
2256
- if (pulled.ok) {
2257
- const result = pulled.value;
2258
- suffix = ` \xB7 ${describePull(result)}`;
2259
- } else {
2260
- suffix = ` \xB7 ${t("notice.pullFailed", { reason: pulled.error.message })}`;
2261
- }
2262
2291
  await refresh();
2292
+ const pushFail = pushed.ok ? void 0 : t("notice.pushFailed", { reason: pushed.error.message });
2293
+ const pullFail = pulled.ok ? void 0 : t("notice.pullFailed", { reason: pulled.error.message });
2294
+ const pushCount = pushed.ok ? writtenBy(pushed.value) : 0;
2295
+ const pullCount = pulled.ok ? arrivedFrom(pulled.value) : 0;
2296
+ const warnings = pulled.ok ? syncWarnings(pulled.value) : "";
2263
2297
  const total = list?.matched;
2298
+ const idle = pushFail === void 0 && pullFail === void 0 && pushCount === 0 && pullCount === 0;
2299
+ const short = idle ? t("sync.shortIdle") : [
2300
+ pushFail ?? t("sync.shortPushed", { count: pushCount }),
2301
+ pullFail ?? t("sync.shortPulled", { count: pullCount })
2302
+ ].join(" \xB7 ");
2264
2303
  setNotice(
2265
- `${pushLine}${suffix}${total === void 0 ? "" : t("sync.thisPage", { count: total })}`
2304
+ `${short}${warnings}${total === void 0 ? "" : t("sync.thisPage", { count: total })}`
2305
+ );
2306
+ setSyncDetail(
2307
+ [
2308
+ pushed.ok ? describePush(pushed.value) : t("notice.pushFailed", { reason: pushed.error.message }),
2309
+ pulled.ok ? describePull(pulled.value) : t("notice.pullFailed", { reason: pulled.error.message })
2310
+ ].join(" \xB7 ")
2266
2311
  );
2267
2312
  } finally {
2268
2313
  setBusy(false);
@@ -2463,6 +2508,28 @@ window.__ModuleLoader__.load({
2463
2508
  if (entry.url !== void 0) setZoom({ href: entry.url, label: headingOf(entry) });
2464
2509
  }, []);
2465
2510
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { ref: panelRef, style: { ...panelStyle, colorScheme: scheme }, children: [
2511
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
2512
+ /*
2513
+ Chromium ignores ::-webkit-scrollbar the moment the standard
2514
+ scrollbar-width is set, so the two engines get the rule they can use:
2515
+ engines that know ::-webkit-scrollbar get the 8px bar, and Firefox \u2014
2516
+ which does not \u2014 falls back to its own thin one. Measured: with both
2517
+ set, Chromium kept its 10px thin bar and the 8px rule did nothing.
2518
+ */
2519
+ @supports not selector(::-webkit-scrollbar) {
2520
+ .dsh-inbox-scroll { scrollbar-width: thin; }
2521
+ }
2522
+ .dsh-inbox-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
2523
+ .dsh-inbox-scroll::-webkit-scrollbar-track { background: transparent; }
2524
+ .dsh-inbox-scroll::-webkit-scrollbar-thumb {
2525
+ background: color-mix(in srgb, currentColor 22%, transparent);
2526
+ border-radius: 8px;
2527
+ }
2528
+ .dsh-inbox-scroll::-webkit-scrollbar-thumb:hover {
2529
+ background: color-mix(in srgb, currentColor 34%, transparent);
2530
+ }
2531
+ .dsh-inbox-scroll::-webkit-scrollbar-corner { background: transparent; }
2532
+ ` }),
2466
2533
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("header", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 10 }, children: [
2467
2534
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
2468
2535
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: { margin: "0 0 4px" }, children: "dsh-inbox" }),
@@ -2867,6 +2934,7 @@ window.__ModuleLoader__.load({
2867
2934
  "button",
2868
2935
  {
2869
2936
  type: "button",
2937
+ title: syncDetail,
2870
2938
  style: { ...buttonStyle, height: CONTROL_HEIGHT, boxSizing: "border-box" },
2871
2939
  disabled: busy,
2872
2940
  onClick: () => void refreshAll(),
@@ -3047,6 +3115,7 @@ window.__ModuleLoader__.load({
3047
3115
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3048
3116
  "div",
3049
3117
  {
3118
+ className: "dsh-inbox-scroll",
3050
3119
  style: listMode === "compact" ? {
3051
3120
  display: "flex",
3052
3121
  flexDirection: "column",
@@ -3215,37 +3284,43 @@ window.__ModuleLoader__.load({
3215
3284
  )
3216
3285
  ] });
3217
3286
  }
3287
+ function writtenBy(result) {
3288
+ return result.status === "ok" || result.status === "partial" ? result.pushed : 0;
3289
+ }
3290
+ function arrivedFrom(result) {
3291
+ return result.status === "ok" ? result.pulled + (result.merged ?? 0) : 0;
3292
+ }
3293
+ function syncWarnings(result) {
3294
+ return warningsOf(result, false);
3295
+ }
3296
+ function warningsOf(result, verbose) {
3297
+ const roots = result.foreignSyncRoots ?? [];
3298
+ const foreign = roots.length === 0 ? "" : verbose ? t("sync.detailForeign", {
3299
+ roots: roots.join("\u3001"),
3300
+ ours: result.syncRoot ?? "",
3301
+ records: result.foreignRecords ?? 0
3302
+ }) : t("sync.pullForeignSync", { roots: roots.join("\u3001"), records: result.foreignRecords ?? 0 });
3303
+ const failed = result.failed > 0 ? t("sync.pullFailures", { count: result.failed, reason: result.reason ?? "" }) : "";
3304
+ return `${foreign}${failed}`;
3305
+ }
3218
3306
  function describePush(result) {
3219
3307
  if (result.status === "unconfigured") return t("sync.pushUnconfigured");
3220
3308
  if (result.status === "failed") return t("sync.pushFailed", { reason: result.reason ?? t("sync.pushUnknown") });
3221
- const head = result.pushed === 0 && result.attachments === 0 ? t("sync.pushUpToDate", { count: result.skipped }) : t("sync.pushDone", { records: result.pushed, attachments: result.attachments });
3309
+ const head = result.pushed === 0 && result.attachments === 0 ? t("sync.detailPushedIdle", { count: result.skipped }) : t("sync.detailPushed", { records: result.pushed, attachments: result.attachments });
3222
3310
  return result.status === "partial" ? t("sync.pushPartial", { head, reason: result.reason ?? "" }) : head;
3223
3311
  }
3224
3312
  function describePull(result) {
3225
3313
  if (result.status === "unconfigured") return result.reason ?? t("sync.pullNoAddress");
3226
3314
  if (result.status === "failed") return t("sync.pullFailed", { reason: result.reason ?? t("sync.pushUnknown") });
3227
3315
  const merged = result.merged ?? 0;
3228
- const attachments = result.attachments ?? 0;
3229
- const syncPart = merged === 0 && attachments === 0 ? t("sync.pullNothingNew") : t("sync.pullMerged", {
3230
- count: merged,
3231
- attachments: attachments === 0 ? "" : t("sync.pullAttachments", { count: attachments })
3232
- });
3233
- if (result.pulled === 0 && result.skipped === 0 && result.failed === 0) return syncPart;
3234
- const skippedWhy = result.skippedSync === void 0 ? "" : t("sync.pullSkipWhy", { sync: result.skippedSync, older: result.skippedOlder ?? 0 });
3235
- const foreignWhy = result.foreignSyncRoots === void 0 || result.foreignSyncRoots.length === 0 ? "" : t("sync.pullForeignSync", {
3236
- roots: result.foreignSyncRoots.join("\u3001"),
3237
- ours: result.syncRoot ?? ""
3238
- });
3239
- const failedWhy = result.failed > 0 ? t("sync.pullFailures", {
3240
- count: result.failed,
3241
- reason: result.reason ?? t("sync.pushUnknown")
3242
- }) : "";
3243
- return t("sync.pullDone", {
3244
- listed: result.listed,
3245
- pulled: result.pulled,
3246
- skipped: result.skipped,
3247
- tail: `${skippedWhy}${syncPart}${foreignWhy}${failedWhy}`
3316
+ const head = t("sync.detailPulled", {
3317
+ count: result.pulled + merged,
3318
+ records: result.remoteRecords ?? 0,
3319
+ files: result.remoteAttachments ?? 0
3248
3320
  });
3321
+ const gained = (result.added ?? 0) === 0 ? "" : t("sync.detailAdded", { count: result.added ?? 0 });
3322
+ const deleted = (result.deletions ?? 0) === 0 ? "" : t("sync.detailDeleted", { count: result.deletions ?? 0 });
3323
+ return `${head}${gained}${deleted}${warningsOf(result, true)}`;
3249
3324
  }
3250
3325
  function EncryptionSettings({ call }) {
3251
3326
  const [status, setStatus] = import_react8.default.useState();
@@ -3340,6 +3415,7 @@ window.__ModuleLoader__.load({
3340
3415
  const [status, setStatus] = import_react8.default.useState();
3341
3416
  const [baseUrl, setBaseUrl] = import_react8.default.useState("");
3342
3417
  const [directory, setDirectory] = import_react8.default.useState("/inbox");
3418
+ const [adoptForeignRoots, setAdoptForeignRoots] = import_react8.default.useState(false);
3343
3419
  const [username, setUsername] = import_react8.default.useState("");
3344
3420
  const [password, setPassword] = import_react8.default.useState("");
3345
3421
  const [protocol, setProtocol] = import_react8.default.useState("webdav");
@@ -3364,6 +3440,7 @@ window.__ModuleLoader__.load({
3364
3440
  setStatus(next);
3365
3441
  setBaseUrl(next.settings.baseUrl);
3366
3442
  setDirectory(next.settings.directory);
3443
+ setAdoptForeignRoots(next.settings.adoptForeignRoots === true);
3367
3444
  setUsername(next.settings.username);
3368
3445
  setProtocol(next.settings.protocol);
3369
3446
  setEndpoint(next.settings.endpoint);
@@ -3393,6 +3470,7 @@ window.__ModuleLoader__.load({
3393
3470
  protocol,
3394
3471
  baseUrl,
3395
3472
  directory,
3473
+ adoptForeignRoots,
3396
3474
  username,
3397
3475
  // Sending nothing leaves the stored password alone; sending "" clears it.
3398
3476
  ...password.length === 0 ? {} : { password },
@@ -3494,11 +3572,15 @@ window.__ModuleLoader__.load({
3494
3572
  value: directory,
3495
3573
  disabled: busy,
3496
3574
  onChange: (event) => setDirectory(event.target.value),
3497
- placeholder: "/inbox",
3575
+ placeholder: t("settings.directoryPlaceholder"),
3498
3576
  style: { ...inputStyle, flex: 1 }
3499
3577
  }
3500
3578
  ),
3501
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.bucketHint") })
3579
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { opacity: 0.6 }, children: [
3580
+ t("settings.bucketHint"),
3581
+ " \xB7 ",
3582
+ t("settings.syncRootNow", { root: syncRootFor(directory) })
3583
+ ] })
3502
3584
  ] }),
3503
3585
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
3504
3586
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.7, minWidth: 64 }, children: t("settings.username") }),
@@ -3612,14 +3694,31 @@ window.__ModuleLoader__.load({
3612
3694
  value: directory,
3613
3695
  disabled: busy,
3614
3696
  onChange: (event) => setDirectory(event.target.value),
3615
- placeholder: "/inbox",
3697
+ placeholder: t("settings.directoryPlaceholder"),
3616
3698
  style: { ...inputStyle, flex: 1 }
3617
3699
  }
3618
3700
  ),
3619
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.syncRootNow", { root: syncRootFor(directory) }) })
3701
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { opacity: 0.6 }, children: [
3702
+ t("settings.bucketHint"),
3703
+ " \xB7 ",
3704
+ t("settings.syncRootNow", { root: syncRootFor(directory) })
3705
+ ] })
3620
3706
  ] })
3621
3707
  ] }),
3622
3708
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
3709
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", gap: 6, alignItems: "center", flex: "1 0 100%" }, children: [
3710
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3711
+ "input",
3712
+ {
3713
+ type: "checkbox",
3714
+ checked: adoptForeignRoots,
3715
+ disabled: busy,
3716
+ onChange: (event) => setAdoptForeignRoots(event.target.checked)
3717
+ }
3718
+ ),
3719
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.adoptForeign") }),
3720
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.adoptForeignHint") })
3721
+ ] }),
3623
3722
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: buttonStyle, disabled: busy, onClick: () => void save(), children: t("settings.save") }),
3624
3723
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: buttonStyle, disabled: busy, onClick: () => void selfTest(), children: t("settings.probe") }),
3625
3724
  detailNotice(notice)
@@ -4050,6 +4149,7 @@ window.__ModuleLoader__.load({
4050
4149
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
4051
4150
  "div",
4052
4151
  {
4152
+ className: "dsh-inbox-scroll",
4053
4153
  style: {
4054
4154
  flex: 1,
4055
4155
  minHeight: 0,