@chance722/dsh-inbox 0.2.3 → 0.2.4

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,23 @@ 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
+ "sync.detailPushedIdle": "\u63A8\u9001 0 \u6761\uFF08{count} \u6761\u5DF2\u662F\u6700\u65B0\uFF09",
872
+ "sync.detailPushed": "\u63A8\u9001 {records} \u6761 / {attachments} \u4E2A\u9644\u4EF6",
873
+ "sync.detailPulled": "\u62C9\u53D6 {count} \u6761 \xB7 \u4E91\u7AEF {records} \u6761\u8BB0\u5F55 / {files} \u4E2A\u9644\u4EF6",
874
+ "sync.detailAdded": "\uFF08\u5176\u4E2D {count} \u6761\u662F\u65B0\u8BB0\u5F55\uFF09",
875
+ "sync.detailSkip": "\uFF08\u8DF3\u8FC7 {skipped}\uFF1A\u81EA\u5DF1\u7684 {sync}\u3001\u66F4\u65E7 {older}\uFF09",
876
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F \u53E6\u6709 {records} \u6761\u8BB0\u5F55\u5728\u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}",
877
+ "sync.detailForeign": " \xB7 \u26A0\uFE0F \u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}\uFF08{records} \u6761\u8BB0\u5F55\uFF1B\u672C\u673A {ours}\uFF09",
878
+ // The toast: two numbers, nothing else. Everything else about a sync is
879
+ // detail, and detail belongs in a line that does not vanish.
880
+ "sync.shortPushed": "\u63A8\u9001 {count} \u6761",
881
+ "sync.shortPulled": "\u62C9\u53D6 {count} \u6761",
882
+ "sync.shortIdle": "\u5DF2\u662F\u6700\u65B0",
872
883
  "sync.pullFailures": " \xB7 \u5931\u8D25 {count}\uFF1A{reason}",
873
884
  "sync.thisPage": " \xB7 \u672C\u9875 {count} \u6761",
874
885
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
@@ -938,10 +949,15 @@ window.__ModuleLoader__.load({
938
949
  "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
950
  "manual.5.manual.label": "\u624B\u52A8",
940
951
  "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",
952
+ // The one rule that decides whether two machines see each other at all.
953
+ "manual.5.twoMachines.label": "\u4E24\u53F0\u673A\u5668",
954
+ "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
955
  "manual.5.delete.label": "\u5220\u9664",
942
956
  "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",
957
+ "manual.5.merge.label": "\u5408\u5E76",
958
+ "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",
959
+ "manual.5.cloud.label": "\u4E0A\u4E91\u4EC0\u4E48",
960
+ "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
961
  "manual.6.title": "6. \u5728\u5BF9\u8BDD\u91CC\u53D6\u56DE\u6765",
946
962
  "manual.6.query.label": "\u67E5",
947
963
  "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 +1084,9 @@ window.__ModuleLoader__.load({
1068
1084
  "settings.region": "Region",
1069
1085
  "settings.bucketAddress": "Address",
1070
1086
  "settings.bucketDir": "Directory",
1087
+ "settings.adoptForeign": "Merge other sync directories too",
1088
+ "settings.adoptForeignHint": "(brings back records a machine left under an older directory; off by default)",
1089
+ "settings.directoryPlaceholder": "inbox (the default \u2014 blank or / means the same)",
1071
1090
  "settings.dirS3.lead": "the directory is also the key prefix for S3 (default ",
1072
1091
  "settings.dirS3.mid": ", which becomes ",
1073
1092
  "settings.dirS3.tail": ").",
@@ -1115,17 +1134,19 @@ window.__ModuleLoader__.load({
1115
1134
  "sync.pushUnconfigured": "Push: no remote configured yet",
1116
1135
  "sync.pushFailed": "Push failed: {reason}",
1117
1136
  "sync.pushUnknown": "unknown reason",
1118
- "sync.pushUpToDate": "Push: nothing new ({count} already current)",
1119
- "sync.pushDone": "Push: {records} records / {attachments} attachment(s)",
1120
1137
  "sync.pushPartial": "{head} \xB7 partly failed: {reason}",
1121
1138
  "sync.pullNoAddress": "No address configured yet",
1122
1139
  "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",
1140
+ "sync.detailPushedIdle": "pushed 0 ({count} already up to date)",
1141
+ "sync.detailPushed": "pushed {records} / {attachments} attachments",
1142
+ "sync.detailPulled": "pulled {count} \xB7 the cloud holds {records} records / {files} attachments",
1143
+ "sync.detailAdded": " ({count} of them new)",
1144
+ "sync.detailSkip": " ({skipped} skipped: {sync} ours, {older} older)",
1145
+ "sync.pullForeignSync": " \xB7 \u26A0\uFE0F {records} records live in another sync directory: {roots}",
1146
+ "sync.detailForeign": " \xB7 \u26A0\uFE0F another sync directory: {roots} ({records} records; ours is {ours})",
1147
+ "sync.shortPushed": "pushed {count}",
1148
+ "sync.shortPulled": "pulled {count}",
1149
+ "sync.shortIdle": "up to date",
1129
1150
  "sync.pullFailures": " \xB7 {count} failed: {reason}",
1130
1151
  "sync.thisPage": " \xB7 {count} on this page",
1131
1152
  "settings.status": "{settings} \xB7 {webdav} \xB7 {s3}",
@@ -1192,10 +1213,14 @@ window.__ModuleLoader__.load({
1192
1213
  "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
1214
  "manual.5.manual.label": "Manual",
1194
1215
  "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.",
1216
+ "manual.5.twoMachines.label": "Two machines",
1217
+ "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
1218
  "manual.5.delete.label": "Deleting",
1196
1219
  "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.",
1220
+ "manual.5.merge.label": "Merging",
1221
+ "manual.5.merge.body": "Incoming records merge by id + timestamp: the newer write wins, no conflict copies.",
1222
+ "manual.5.cloud.label": "What goes up",
1223
+ "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
1224
  "manual.6.title": "6. Getting things back in the conversation",
1200
1225
  "manual.6.query.label": "Ask",
1201
1226
  "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 +1783,12 @@ window.__ModuleLoader__.load({
1758
1783
  manual ran off the bottom of the screen with no visible frame.
1759
1784
  */
1760
1785
  width: "min(560px, 100%)",
1761
- maxHeight: "92vh",
1786
+ /*
1787
+ 80vh rather than 92vh: at 92 the card still felt like a full screen
1788
+ and it pushed the panel out of sight, while the point of the manual
1789
+ is to be readable *beside* what it explains (asked 2026-09-21).
1790
+ */
1791
+ maxHeight: "80vh",
1762
1792
  // Border inside the width and the cap: without it the card is 2px
1763
1793
  // wider than the number and 2px taller than the overlay's room.
1764
1794
  boxSizing: "border-box",
@@ -1809,6 +1839,7 @@ window.__ModuleLoader__.load({
1809
1839
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1810
1840
  "div",
1811
1841
  {
1842
+ className: "dsh-inbox-scroll",
1812
1843
  style: {
1813
1844
  padding: 18,
1814
1845
  display: "flex",
@@ -1852,8 +1883,10 @@ window.__ModuleLoader__.load({
1852
1883
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Section, { title: t("manual.5.title"), children: [
1853
1884
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.auto.label"), children: t("manual.5.auto.body") }),
1854
1885
  /* @__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") })
1886
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.merge.label"), children: t("manual.5.merge.body") }),
1887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.twoMachines.label"), children: t("manual.5.twoMachines.body") }),
1888
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.cloud.label"), children: t("manual.5.cloud.body") }),
1889
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.5.delete.label"), children: t("manual.5.delete.body") })
1857
1890
  ] }),
1858
1891
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Section, { title: t("manual.6.title"), children: [
1859
1892
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Line, { label: t("manual.6.query.label"), children: t("manual.6.query.body") }),
@@ -2098,6 +2131,7 @@ window.__ModuleLoader__.load({
2098
2131
  const [text, setText] = import_react8.default.useState("");
2099
2132
  const [staged, setStaged] = import_react8.default.useState([]);
2100
2133
  const [notice, setNotice] = import_react8.default.useState();
2134
+ const [syncDetail, setSyncDetail] = import_react8.default.useState();
2101
2135
  const [busy, setBusy] = import_react8.default.useState(false);
2102
2136
  const [dragging, setDragging] = import_react8.default.useState(false);
2103
2137
  const picker = import_react8.default.useRef(null);
@@ -2250,19 +2284,27 @@ window.__ModuleLoader__.load({
2250
2284
  setNotice(t("notice.syncing"));
2251
2285
  try {
2252
2286
  const pushed = await call(INBOX_ENDPOINT_PUSH, {});
2253
- const pushLine = pushed.ok ? describePush(pushed.value) : t("notice.pushFailed", { reason: pushed.error.message });
2254
2287
  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
2288
  await refresh();
2289
+ const pushFail = pushed.ok ? void 0 : t("notice.pushFailed", { reason: pushed.error.message });
2290
+ const pullFail = pulled.ok ? void 0 : t("notice.pullFailed", { reason: pulled.error.message });
2291
+ const pushCount = pushed.ok ? writtenBy(pushed.value) : 0;
2292
+ const pullCount = pulled.ok ? arrivedFrom(pulled.value) : 0;
2293
+ const warnings = pulled.ok ? syncWarnings(pulled.value) : "";
2263
2294
  const total = list?.matched;
2295
+ const idle = pushFail === void 0 && pullFail === void 0 && pushCount === 0 && pullCount === 0;
2296
+ const short = idle ? t("sync.shortIdle") : [
2297
+ pushFail ?? t("sync.shortPushed", { count: pushCount }),
2298
+ pullFail ?? t("sync.shortPulled", { count: pullCount })
2299
+ ].join(" \xB7 ");
2264
2300
  setNotice(
2265
- `${pushLine}${suffix}${total === void 0 ? "" : t("sync.thisPage", { count: total })}`
2301
+ `${short}${warnings}${total === void 0 ? "" : t("sync.thisPage", { count: total })}`
2302
+ );
2303
+ setSyncDetail(
2304
+ [
2305
+ pushed.ok ? describePush(pushed.value) : t("notice.pushFailed", { reason: pushed.error.message }),
2306
+ pulled.ok ? describePull(pulled.value) : t("notice.pullFailed", { reason: pulled.error.message })
2307
+ ].join(" \xB7 ")
2266
2308
  );
2267
2309
  } finally {
2268
2310
  setBusy(false);
@@ -2463,6 +2505,28 @@ window.__ModuleLoader__.load({
2463
2505
  if (entry.url !== void 0) setZoom({ href: entry.url, label: headingOf(entry) });
2464
2506
  }, []);
2465
2507
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { ref: panelRef, style: { ...panelStyle, colorScheme: scheme }, children: [
2508
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
2509
+ /*
2510
+ Chromium ignores ::-webkit-scrollbar the moment the standard
2511
+ scrollbar-width is set, so the two engines get the rule they can use:
2512
+ engines that know ::-webkit-scrollbar get the 8px bar, and Firefox \u2014
2513
+ which does not \u2014 falls back to its own thin one. Measured: with both
2514
+ set, Chromium kept its 10px thin bar and the 8px rule did nothing.
2515
+ */
2516
+ @supports not selector(::-webkit-scrollbar) {
2517
+ .dsh-inbox-scroll { scrollbar-width: thin; }
2518
+ }
2519
+ .dsh-inbox-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
2520
+ .dsh-inbox-scroll::-webkit-scrollbar-track { background: transparent; }
2521
+ .dsh-inbox-scroll::-webkit-scrollbar-thumb {
2522
+ background: color-mix(in srgb, currentColor 22%, transparent);
2523
+ border-radius: 8px;
2524
+ }
2525
+ .dsh-inbox-scroll::-webkit-scrollbar-thumb:hover {
2526
+ background: color-mix(in srgb, currentColor 34%, transparent);
2527
+ }
2528
+ .dsh-inbox-scroll::-webkit-scrollbar-corner { background: transparent; }
2529
+ ` }),
2466
2530
  /* @__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
2531
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
2468
2532
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: { margin: "0 0 4px" }, children: "dsh-inbox" }),
@@ -2867,6 +2931,7 @@ window.__ModuleLoader__.load({
2867
2931
  "button",
2868
2932
  {
2869
2933
  type: "button",
2934
+ title: syncDetail,
2870
2935
  style: { ...buttonStyle, height: CONTROL_HEIGHT, boxSizing: "border-box" },
2871
2936
  disabled: busy,
2872
2937
  onClick: () => void refreshAll(),
@@ -3047,6 +3112,7 @@ window.__ModuleLoader__.load({
3047
3112
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3048
3113
  "div",
3049
3114
  {
3115
+ className: "dsh-inbox-scroll",
3050
3116
  style: listMode === "compact" ? {
3051
3117
  display: "flex",
3052
3118
  flexDirection: "column",
@@ -3215,37 +3281,47 @@ window.__ModuleLoader__.load({
3215
3281
  )
3216
3282
  ] });
3217
3283
  }
3284
+ function writtenBy(result) {
3285
+ return result.status === "ok" || result.status === "partial" ? result.pushed : 0;
3286
+ }
3287
+ function arrivedFrom(result) {
3288
+ return result.status === "ok" ? result.pulled + (result.merged ?? 0) : 0;
3289
+ }
3290
+ function syncWarnings(result) {
3291
+ return warningsOf(result, false);
3292
+ }
3293
+ function warningsOf(result, verbose) {
3294
+ const roots = result.foreignSyncRoots ?? [];
3295
+ const foreign = roots.length === 0 ? "" : verbose ? t("sync.detailForeign", {
3296
+ roots: roots.join("\u3001"),
3297
+ ours: result.syncRoot ?? "",
3298
+ records: result.foreignRecords ?? 0
3299
+ }) : t("sync.pullForeignSync", { roots: roots.join("\u3001"), records: result.foreignRecords ?? 0 });
3300
+ const failed = result.failed > 0 ? t("sync.pullFailures", { count: result.failed, reason: result.reason ?? "" }) : "";
3301
+ return `${foreign}${failed}`;
3302
+ }
3218
3303
  function describePush(result) {
3219
3304
  if (result.status === "unconfigured") return t("sync.pushUnconfigured");
3220
3305
  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 });
3306
+ const head = result.pushed === 0 && result.attachments === 0 ? t("sync.detailPushedIdle", { count: result.skipped }) : t("sync.detailPushed", { records: result.pushed, attachments: result.attachments });
3222
3307
  return result.status === "partial" ? t("sync.pushPartial", { head, reason: result.reason ?? "" }) : head;
3223
3308
  }
3224
3309
  function describePull(result) {
3225
3310
  if (result.status === "unconfigured") return result.reason ?? t("sync.pullNoAddress");
3226
3311
  if (result.status === "failed") return t("sync.pullFailed", { reason: result.reason ?? t("sync.pushUnknown") });
3227
3312
  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 ?? ""
3313
+ const head = t("sync.detailPulled", {
3314
+ count: result.pulled + merged,
3315
+ records: result.remoteRecords ?? 0,
3316
+ files: result.remoteAttachments ?? 0
3238
3317
  });
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,
3318
+ const gained = (result.added ?? 0) === 0 ? "" : t("sync.detailAdded", { count: result.added ?? 0 });
3319
+ const skipped = result.skipped === 0 ? "" : t("sync.detailSkip", {
3246
3320
  skipped: result.skipped,
3247
- tail: `${skippedWhy}${syncPart}${foreignWhy}${failedWhy}`
3321
+ sync: result.skippedSync ?? 0,
3322
+ older: result.skippedOlder ?? 0
3248
3323
  });
3324
+ return `${head}${gained}${skipped}${warningsOf(result, true)}`;
3249
3325
  }
3250
3326
  function EncryptionSettings({ call }) {
3251
3327
  const [status, setStatus] = import_react8.default.useState();
@@ -3340,6 +3416,7 @@ window.__ModuleLoader__.load({
3340
3416
  const [status, setStatus] = import_react8.default.useState();
3341
3417
  const [baseUrl, setBaseUrl] = import_react8.default.useState("");
3342
3418
  const [directory, setDirectory] = import_react8.default.useState("/inbox");
3419
+ const [adoptForeignRoots, setAdoptForeignRoots] = import_react8.default.useState(false);
3343
3420
  const [username, setUsername] = import_react8.default.useState("");
3344
3421
  const [password, setPassword] = import_react8.default.useState("");
3345
3422
  const [protocol, setProtocol] = import_react8.default.useState("webdav");
@@ -3364,6 +3441,7 @@ window.__ModuleLoader__.load({
3364
3441
  setStatus(next);
3365
3442
  setBaseUrl(next.settings.baseUrl);
3366
3443
  setDirectory(next.settings.directory);
3444
+ setAdoptForeignRoots(next.settings.adoptForeignRoots === true);
3367
3445
  setUsername(next.settings.username);
3368
3446
  setProtocol(next.settings.protocol);
3369
3447
  setEndpoint(next.settings.endpoint);
@@ -3393,6 +3471,7 @@ window.__ModuleLoader__.load({
3393
3471
  protocol,
3394
3472
  baseUrl,
3395
3473
  directory,
3474
+ adoptForeignRoots,
3396
3475
  username,
3397
3476
  // Sending nothing leaves the stored password alone; sending "" clears it.
3398
3477
  ...password.length === 0 ? {} : { password },
@@ -3494,11 +3573,15 @@ window.__ModuleLoader__.load({
3494
3573
  value: directory,
3495
3574
  disabled: busy,
3496
3575
  onChange: (event) => setDirectory(event.target.value),
3497
- placeholder: "/inbox",
3576
+ placeholder: t("settings.directoryPlaceholder"),
3498
3577
  style: { ...inputStyle, flex: 1 }
3499
3578
  }
3500
3579
  ),
3501
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.bucketHint") })
3580
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { opacity: 0.6 }, children: [
3581
+ t("settings.bucketHint"),
3582
+ " \xB7 ",
3583
+ t("settings.syncRootNow", { root: syncRootFor(directory) })
3584
+ ] })
3502
3585
  ] }),
3503
3586
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
3504
3587
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.7, minWidth: 64 }, children: t("settings.username") }),
@@ -3612,14 +3695,31 @@ window.__ModuleLoader__.load({
3612
3695
  value: directory,
3613
3696
  disabled: busy,
3614
3697
  onChange: (event) => setDirectory(event.target.value),
3615
- placeholder: "/inbox",
3698
+ placeholder: t("settings.directoryPlaceholder"),
3616
3699
  style: { ...inputStyle, flex: 1 }
3617
3700
  }
3618
3701
  ),
3619
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.syncRootNow", { root: syncRootFor(directory) }) })
3702
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { opacity: 0.6 }, children: [
3703
+ t("settings.bucketHint"),
3704
+ " \xB7 ",
3705
+ t("settings.syncRootNow", { root: syncRootFor(directory) })
3706
+ ] })
3620
3707
  ] })
3621
3708
  ] }),
3622
3709
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
3710
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { style: { display: "flex", gap: 6, alignItems: "center", flex: "1 0 100%" }, children: [
3711
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3712
+ "input",
3713
+ {
3714
+ type: "checkbox",
3715
+ checked: adoptForeignRoots,
3716
+ disabled: busy,
3717
+ onChange: (event) => setAdoptForeignRoots(event.target.checked)
3718
+ }
3719
+ ),
3720
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.adoptForeign") }),
3721
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { opacity: 0.6 }, children: t("settings.adoptForeignHint") })
3722
+ ] }),
3623
3723
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: buttonStyle, disabled: busy, onClick: () => void save(), children: t("settings.save") }),
3624
3724
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: buttonStyle, disabled: busy, onClick: () => void selfTest(), children: t("settings.probe") }),
3625
3725
  detailNotice(notice)
@@ -4050,6 +4150,7 @@ window.__ModuleLoader__.load({
4050
4150
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
4051
4151
  "div",
4052
4152
  {
4153
+ className: "dsh-inbox-scroll",
4053
4154
  style: {
4054
4155
  flex: 1,
4055
4156
  minHeight: 0,