selfstore 1.8.11 → 1.8.13

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/dist/widgets.js CHANGED
@@ -130,7 +130,23 @@ img[part~='icon'] {
130
130
  background: color-mix(in srgb, var(--_accent) 10%, transparent);
131
131
  }
132
132
  [part~='status-ok'] { background: color-mix(in srgb, var(--_ok) 12%, transparent); }
133
+ [part~='status-warn'] { background: color-mix(in srgb, var(--_warn) 12%, transparent); }
133
134
  [part~='status-error'] { background: color-mix(in srgb, var(--_danger) 12%, transparent); }
135
+ /* The state, said in one glyph before the sentence is read: a saved backup is a
136
+ tick, anything asking for a gesture is a mark. It carries the severity color
137
+ the dot would have carried, and reads at a glance from across the page. */
138
+ span[part~='status-glyph'] {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ width: 1.5em;
143
+ height: 1.5em;
144
+ border-radius: 50%;
145
+ flex-shrink: 0;
146
+ font-size: 0.85em;
147
+ font-weight: 700;
148
+ background: color-mix(in srgb, currentColor 15%, transparent);
149
+ }
134
150
  [part='spinner'] {
135
151
  width: 0.7em; height: 0.7em; flex-shrink: 0;
136
152
  border-radius: 50%;
@@ -406,6 +422,42 @@ var FlowWidget = class extends HTMLElement {
406
422
  packs() {
407
423
  return {};
408
424
  }
425
+ #store = null;
426
+ #icons = {};
427
+ /**
428
+ * The store this widget reads: the simple facade (anything exposing
429
+ * `flowHost`) or a hand-built FlowHost. Idempotent, so a host may assign it
430
+ * from a reactive effect that runs more than once.
431
+ *
432
+ * Every widget over a store had written this same pair, plus the host lookup
433
+ * and the glyph map beside it - the same fifteen lines in six files, each one
434
+ * a place where a fix could land in five.
435
+ */
436
+ get store() {
437
+ return this.#store;
438
+ }
439
+ set store(v) {
440
+ if (v === this.#store) return;
441
+ this.#store = v;
442
+ this.storeChanged();
443
+ this.follow(this.hostOf());
444
+ }
445
+ /** Called before the widget follows a newly assigned store, for one that has
446
+ * its own view state to drop (a journey in progress belongs to the old one). */
447
+ storeChanged() {
448
+ }
449
+ /** The ports behind the current store, or null while it is unwired. */
450
+ hostOf() {
451
+ return hostOf(this.#store);
452
+ }
453
+ /** Optional glyph per destination kind, same contract in every widget. */
454
+ get icons() {
455
+ return this.#icons;
456
+ }
457
+ set icons(v) {
458
+ this.#icons = v ?? {};
459
+ this.rerender();
460
+ }
409
461
  /** Follow an engine: rerender on every notification, replacing any previous
410
462
  * subscription. A no-op before connection and on a null host, so a host may
411
463
  * assign the store from a reactive effect that runs more than once. */
@@ -460,6 +512,21 @@ var FlowWidget = class extends HTMLElement {
460
512
  if (!vars) return copy;
461
513
  return copy.replace(/\{(\w+)\}/g, (whole, name) => vars[name] ?? whole);
462
514
  }
515
+ /**
516
+ * "just now", "3 minutes ago", "yesterday" - in the widget's language, from
517
+ * Intl rather than from five label keys per language. The word for the first
518
+ * minute is passed in: it is the one part Intl says badly ("this minute"),
519
+ * and each widget words it in its own register.
520
+ */
521
+ since(ts, justNow) {
522
+ const mins = Math.floor((Date.now() - ts) / 6e4);
523
+ if (mins < 1) return justNow;
524
+ const rtf = new Intl.RelativeTimeFormat(this.langTag(), { numeric: "auto" });
525
+ if (mins < 60) return rtf.format(-mins, "minute");
526
+ const hours = Math.floor(mins / 60);
527
+ if (hours < 24) return rtf.format(-hours, "hour");
528
+ return new Date(ts).toLocaleDateString(this.langTag());
529
+ }
463
530
  /** A heading an empty-string label removes: labels = { 'share.title': '' }
464
531
  * hides that heading (the host page already says it) without a fork.
465
532
  * Append through put(), which skips the null. */
@@ -1993,8 +2060,25 @@ var SelfstoreJoinElement = class extends FlowWidget {
1993
2060
  }
1994
2061
  };
1995
2062
 
1996
- // src/widgets/status.ts
2063
+ // src/widgets/kinds.ts
1997
2064
  var EN4 = {
2065
+ "destination.kind.drive": "Google Drive",
2066
+ "destination.kind.file": "A file on this device",
2067
+ "destination.kind.webdav": "Your own server",
2068
+ "destination.kind.s3": "Your own bucket",
2069
+ "destination.kind.device": "This device only"
2070
+ };
2071
+ var FR4 = {
2072
+ "destination.kind.file": "Un fichier sur cet appareil",
2073
+ "destination.kind.webdav": "Votre serveur",
2074
+ "destination.kind.s3": "Votre bucket",
2075
+ "destination.kind.device": "Cet appareil seulement"
2076
+ };
2077
+
2078
+ // src/widgets/status.ts
2079
+ var EN5 = {
2080
+ ...EN4,
2081
+ "status.justNow": "just now",
1998
2082
  "status.ephemeral": "Nothing is saved",
1999
2083
  "status.cacheOnly": "Never saved anywhere yet",
2000
2084
  "status.saving": "Saving...",
@@ -2008,7 +2092,9 @@ var EN4 = {
2008
2092
  "status.action.reconnect": "Reconnect",
2009
2093
  "status.action.unlock": "Unlock"
2010
2094
  };
2011
- var FR4 = {
2095
+ var FR5 = {
2096
+ ...FR4,
2097
+ "status.justNow": "a l'instant",
2012
2098
  "status.ephemeral": "Rien n'est enregistr\xE9",
2013
2099
  "status.cacheOnly": "Vos donn\xE9es n'ont jamais \xE9t\xE9 enregistr\xE9es ailleurs",
2014
2100
  "status.saving": "Enregistrement...",
@@ -2026,23 +2112,12 @@ var SelfstoreStatusElement = class extends FlowWidget {
2026
2112
  static get observedAttributes() {
2027
2113
  return ["variant"];
2028
2114
  }
2029
- #store = null;
2030
2115
  #variant = "row";
2031
- #icons = {};
2032
2116
  defaults() {
2033
- return EN4;
2117
+ return EN5;
2034
2118
  }
2035
2119
  packs() {
2036
- return { fr: FR4 };
2037
- }
2038
- /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
2039
- get store() {
2040
- return this.#store;
2041
- }
2042
- set store(v) {
2043
- if (v === this.#store) return;
2044
- this.#store = v;
2045
- this.wire();
2120
+ return { fr: FR5 };
2046
2121
  }
2047
2122
  /** 'row' (dot + text + action) or 'dot' (the dot alone). Also the attribute. */
2048
2123
  get variant() {
@@ -2052,31 +2127,16 @@ var SelfstoreStatusElement = class extends FlowWidget {
2052
2127
  this.#variant = v === "dot" ? "dot" : "row";
2053
2128
  this.rerender();
2054
2129
  }
2055
- /** Optional icon per target kind (e.g. { drive: url, file: url }), shown
2056
- * before the text in the row variant. Same contract as the connect cards. */
2057
- get icons() {
2058
- return this.#icons;
2059
- }
2060
- set icons(v) {
2061
- this.#icons = v ?? {};
2062
- this.rerender();
2063
- }
2064
2130
  attributeChangedCallback(name, _old, value) {
2065
2131
  if (name === "variant") this.variant = value;
2066
2132
  }
2067
2133
  connectedCallback() {
2068
2134
  super.connectedCallback();
2069
2135
  if (this.hasAttribute("variant")) this.variant = this.getAttribute("variant");
2070
- this.wire();
2071
- }
2072
- host() {
2073
- return hostOf(this.#store);
2074
- }
2075
- wire() {
2076
- this.follow(this.host());
2136
+ this.follow(this.hostOf());
2077
2137
  }
2078
2138
  view(into) {
2079
- const host = this.host();
2139
+ const host = this.hostOf();
2080
2140
  if (!host) return;
2081
2141
  const { status, targetKind, label } = host.engine.state;
2082
2142
  const sansLieu = (cle) => {
@@ -2084,6 +2144,7 @@ var SelfstoreStatusElement = class extends FlowWidget {
2084
2144
  const copie = this.t(jumelle);
2085
2145
  return copie === jumelle ? this.t(cle) : copie;
2086
2146
  };
2147
+ const { lastSavedAt } = host.engine.state;
2087
2148
  const text = label ? this.t(status.labelKey, { label }) : sansLieu(status.labelKey);
2088
2149
  const dot = h("span", {
2089
2150
  part: `status-dot sev-${status.severity}`,
@@ -2104,20 +2165,36 @@ var SelfstoreStatusElement = class extends FlowWidget {
2104
2165
  );
2105
2166
  return;
2106
2167
  }
2107
- const icon = this.#icons[targetKind];
2168
+ const TINT = {
2169
+ ok: "status-ok",
2170
+ warn: "status-warn",
2171
+ danger: "status-error",
2172
+ info: ""
2173
+ };
2174
+ const GLYPH = { ok: "\u2713", warn: "!", danger: "!", info: "" };
2175
+ const glyph = GLYPH[status.severity];
2176
+ const icon = this.icons[targetKind];
2108
2177
  const where = this.t(`status.where.${targetKind}`);
2109
- const sub = where !== `status.where.${targetKind}` ? where : label ?? "";
2178
+ const kindName = this.t(`destination.kind.${targetKind}`);
2179
+ const named = kindName === `destination.kind.${targetKind}` ? null : kindName;
2180
+ const own = targetKind === "drive" ? null : label;
2181
+ const place = where !== `status.where.${targetKind}` ? where : own ?? named ?? label ?? "";
2182
+ const when = lastSavedAt ? this.since(lastSavedAt, this.t("status.justNow")) : "";
2183
+ const sub = [place, status.state === "saved" ? when : ""].filter(Boolean).join(", ");
2184
+ const title = sub ? sansLieu(status.labelKey) : text;
2110
2185
  put(
2111
2186
  into,
2112
2187
  h(
2113
2188
  "div",
2114
- { part: "row status-row" },
2115
- dot,
2189
+ { part: `status row status-row ${TINT[status.severity] ?? ""}` },
2190
+ // The severity colors the glyph, not the box: tinting the whole line
2191
+ // turns a title into a warning about itself.
2192
+ glyph ? h("span", { part: `status-glyph sev-${status.severity}`, "aria-hidden": "true" }, glyph) : dot,
2116
2193
  icon ? h("img", { part: "icon", src: icon, alt: "" }) : null,
2117
2194
  h(
2118
2195
  "div",
2119
2196
  {},
2120
- h("div", { part: "title" }, text),
2197
+ h("div", { part: "title" }, title),
2121
2198
  sub ? h("div", { part: "sub" }, sub) : null
2122
2199
  ),
2123
2200
  status.actionable && status.action ? h(
@@ -2134,7 +2211,7 @@ var SelfstoreStatusElement = class extends FlowWidget {
2134
2211
  };
2135
2212
 
2136
2213
  // src/widgets/backups.ts
2137
- var EN5 = {
2214
+ var EN6 = {
2138
2215
  "backups.mine.heading": "My backups",
2139
2216
  "backups.mine.personal": "Main backup",
2140
2217
  "backups.active": "Active",
@@ -2191,7 +2268,7 @@ var EN5 = {
2191
2268
  "backups.replica.s3.secret": "Secret access key",
2192
2269
  "error.generic": "That did not work. Check the connection and try again."
2193
2270
  };
2194
- var FR5 = {
2271
+ var FR6 = {
2195
2272
  "backups.mine.heading": "Sauvegardes",
2196
2273
  "backups.mine.personal": "Sauvegarde principale",
2197
2274
  "backups.active": "Active",
@@ -2300,10 +2377,10 @@ var SelfstoreBackupsElement = class extends FlowWidget {
2300
2377
  #withShare = true;
2301
2378
  #withShared = true;
2302
2379
  defaults() {
2303
- return EN5;
2380
+ return EN6;
2304
2381
  }
2305
2382
  packs() {
2306
- return { fr: FR5 };
2383
+ return { fr: FR6 };
2307
2384
  }
2308
2385
  /** The headless manager this panel drives (see 'selfstore/backups'). */
2309
2386
  get manager() {
@@ -3318,14 +3395,14 @@ var SelfstoreBackupsElement = class extends FlowWidget {
3318
3395
  var pillStyle = "display:inline-block;font-size:0.72em;font-weight:600;line-height:1.6;padding:0 0.55em;border-radius:999px;border:1px solid var(--_border)";
3319
3396
 
3320
3397
  // src/widgets/gate.ts
3321
- var EN6 = {
3398
+ var EN7 = {
3322
3399
  "gate.title": "Where should your data live?",
3323
3400
  "gate.hint": "It is encrypted on this device. Choose where to keep it and it gets saved there for you from now on.",
3324
3401
  "gate.fine": "",
3325
3402
  "gate.defer": "Later - keep it in this browser only",
3326
3403
  "gate.defer.note": "Data kept only in this browser is lost if its storage is cleared."
3327
3404
  };
3328
- var FR6 = {
3405
+ var FR7 = {
3329
3406
  "gate.title": "O\xF9 enregistrer vos donn\xE9es ?",
3330
3407
  "gate.hint": "Elles sont chiffr\xE9es sur cet appareil. Choisissez une sauvegarde : tout y sera enregistr\xE9 automatiquement.",
3331
3408
  "gate.fine": "",
@@ -3389,10 +3466,10 @@ var SelfstoreGateElement = class extends FlowWidget {
3389
3466
  #open = false;
3390
3467
  #connect = null;
3391
3468
  defaults() {
3392
- return EN6;
3469
+ return EN7;
3393
3470
  }
3394
3471
  packs() {
3395
- return { fr: FR6 };
3472
+ return { fr: FR7 };
3396
3473
  }
3397
3474
  /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
3398
3475
  get store() {
@@ -3628,40 +3705,31 @@ var SelfstoreGateElement = class extends FlowWidget {
3628
3705
  }
3629
3706
  };
3630
3707
 
3631
- // src/widgets/kinds.ts
3632
- var EN7 = {
3633
- "destination.kind.drive": "Google Drive",
3634
- "destination.kind.file": "A file on this device",
3635
- "destination.kind.webdav": "Your own server",
3636
- "destination.kind.s3": "Your own bucket",
3637
- "destination.kind.device": "This device only"
3638
- };
3639
- var FR7 = {
3640
- "destination.kind.file": "Un fichier sur cet appareil",
3641
- "destination.kind.webdav": "Votre serveur",
3642
- "destination.kind.s3": "Votre bucket",
3643
- "destination.kind.device": "Cet appareil seulement"
3644
- };
3645
-
3646
3708
  // src/widgets/destination.ts
3647
3709
  var EN8 = {
3648
- ...EN7,
3710
+ ...EN4,
3649
3711
  "destination.heading": "Where your data is kept",
3650
3712
  "destination.encrypted": "Password-protected",
3713
+ "destination.active": "Active",
3714
+ "destination.reach.drive": "On all your devices",
3715
+ "destination.reach.file": "",
3651
3716
  "destination.export": "Export a copy",
3652
3717
  "destination.exported": "The copy is written.",
3653
3718
  "destination.exportCancelled": "Nothing was written.",
3654
3719
  "destination.restore": "Restore a copy",
3655
- "destination.change": "Change destination",
3720
+ "destination.change": "Change backup",
3656
3721
  "destination.detach": "Stop saving here",
3657
3722
  "destination.detached": "Stopped. Your data stays on this device.",
3658
3723
  "destination.back": "Back",
3659
3724
  "error.generic": "Something went wrong."
3660
3725
  };
3661
3726
  var FR8 = {
3662
- ...FR7,
3727
+ ...FR4,
3663
3728
  "destination.heading": "O\xF9 vos donn\xE9es sont enregistr\xE9es",
3664
3729
  "destination.encrypted": "Prot\xE9g\xE9e par un mot de passe",
3730
+ "destination.active": "Active",
3731
+ "destination.reach.drive": "Sur tous vos appareils",
3732
+ "destination.reach.file": "",
3665
3733
  "destination.export": "Exporter une copie",
3666
3734
  "destination.exported": "La copie est \xE9crite.",
3667
3735
  "destination.exportCancelled": "Rien n'a \xE9t\xE9 \xE9crit.",
@@ -3689,12 +3757,9 @@ var DESTINATION_STYLES = `
3689
3757
  }
3690
3758
  `;
3691
3759
  var SelfstoreDestinationElement = class extends FlowWidget {
3692
- #store = null;
3693
3760
  #targets = null;
3694
3761
  #account = null;
3695
- #icons = {};
3696
3762
  #confirm = null;
3697
- #changing = false;
3698
3763
  #busy = false;
3699
3764
  #message = null;
3700
3765
  defaults() {
@@ -3707,15 +3772,6 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3707
3772
  super();
3708
3773
  this.root.append(h("style", {}, DESTINATION_STYLES));
3709
3774
  }
3710
- /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
3711
- get store() {
3712
- return this.#store;
3713
- }
3714
- set store(v) {
3715
- if (v === this.#store) return;
3716
- this.#store = v;
3717
- this.wire();
3718
- }
3719
3775
  /** The destinations offered when the user changes their mind. Given them, the
3720
3776
  * panel runs the connect journey in place; without them the button is absent
3721
3777
  * rather than dead - a host that cannot offer a choice should not show one. */
@@ -3736,14 +3792,6 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3736
3792
  this.#account = v || null;
3737
3793
  this.rerender();
3738
3794
  }
3739
- /** Optional glyph per destination kind, same contract as the connect cards. */
3740
- get icons() {
3741
- return this.#icons;
3742
- }
3743
- set icons(v) {
3744
- this.#icons = v ?? {};
3745
- this.rerender();
3746
- }
3747
3795
  /** Veto on what is destructive. Returns false (or throws) to stop; absent
3748
3796
  * means proceed. The panel never invents its own confirm dialog: an app has
3749
3797
  * its own words and its own modal. */
@@ -3755,13 +3803,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3755
3803
  }
3756
3804
  connectedCallback() {
3757
3805
  super.connectedCallback();
3758
- this.wire();
3759
- }
3760
- host() {
3761
- return hostOf(this.#store);
3762
- }
3763
- wire() {
3764
- this.follow(this.host());
3806
+ this.follow(this.hostOf());
3765
3807
  }
3766
3808
  /** Run a gesture, keeping the panel honest while it is in flight. */
3767
3809
  async run(job) {
@@ -3781,7 +3823,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3781
3823
  * nothing, so it must not clear the pending-export nudge nor claim success -
3782
3824
  * that is the download that lies about itself. */
3783
3825
  exportCopy() {
3784
- const host = this.host();
3826
+ const host = this.hostOf();
3785
3827
  if (!host) return Promise.resolve();
3786
3828
  return this.run(async () => {
3787
3829
  const blob = await host.engine.exportBlob();
@@ -3794,7 +3836,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3794
3836
  /** Stop saving to this destination. The local data stays, and so does the
3795
3837
  * backup already written: this detaches, it never deletes. */
3796
3838
  detach(label) {
3797
- const host = this.host();
3839
+ const host = this.hostOf();
3798
3840
  if (!host) return Promise.resolve();
3799
3841
  return this.run(async () => {
3800
3842
  try {
@@ -3807,49 +3849,21 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3807
3849
  return this.t("destination.detached");
3808
3850
  });
3809
3851
  }
3810
- /** The connect journey, in place. Built like the gate builds it, so the
3811
- * destinations, the password step and the cancel path are the library's
3812
- * tested ones rather than a second implementation. */
3813
- connectView(into) {
3814
- const el = document.createElement(
3815
- siblingTag(this.localName, "destination", "connect")
3816
- );
3817
- el.store = this.#store;
3818
- el.targets = this.#targets;
3819
- el.icons = this.#icons;
3820
- el.labels = this.labels;
3821
- const done = () => {
3822
- this.#changing = false;
3823
- this.rerender();
3824
- };
3825
- el.addEventListener("selfstore-connected", done);
3826
- el.addEventListener("selfstore-cancelled", done);
3827
- put(
3828
- into,
3829
- h(
3830
- "button",
3831
- { part: "link destination-back", type: "button", onclick: done },
3832
- this.t("destination.back")
3833
- ),
3834
- el
3835
- );
3836
- }
3837
3852
  view(into) {
3838
- const host = this.host();
3853
+ const host = this.hostOf();
3839
3854
  if (!host) return;
3840
- if (this.#changing && this.#targets) return this.connectView(into);
3841
3855
  const { targetKind, label, encrypted } = host.engine.state;
3842
3856
  const kind = this.t(`destination.kind.${targetKind}`);
3843
3857
  const title = kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
3844
- const sub = [this.#account, label && label !== title ? label : null].filter(Boolean).join(" \xB7 ");
3845
- const icon = this.#icons[targetKind];
3858
+ const sub = this.#account || (label && label !== title ? label : null);
3859
+ const icon = this.icons[targetKind];
3846
3860
  const status = document.createElement(
3847
3861
  siblingTag(this.localName, "destination", "status")
3848
3862
  );
3849
3863
  status.setAttribute("variant", "row");
3850
3864
  status.labels = this.labels;
3851
- status.icons = this.#icons;
3852
- status.store = this.#store;
3865
+ status.icons = this.icons;
3866
+ status.store = this.store;
3853
3867
  const act = (key, part, onclick) => {
3854
3868
  const label2 = this.t(key);
3855
3869
  if (!label2) return null;
@@ -3859,8 +3873,14 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3859
3873
  label2
3860
3874
  );
3861
3875
  };
3876
+ const durable = targetKind !== "device" && targetKind !== "ephemeral";
3877
+ const reach = this.t(`destination.reach.${targetKind}`);
3878
+ const line = [sub, reach === `destination.reach.${targetKind}` ? null : reach].filter(Boolean).join(" \xB7 ");
3862
3879
  put(
3863
3880
  into,
3881
+ // The situation first, then what holds it: a panel that opens on a card
3882
+ // makes the reader work out from the name whether anything is wrong.
3883
+ status,
3864
3884
  this.heading("destination-heading", "destination.heading"),
3865
3885
  h(
3866
3886
  "div",
@@ -3869,12 +3889,18 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3869
3889
  h(
3870
3890
  "div",
3871
3891
  {},
3872
- h("div", { part: "title" }, title),
3873
- sub ? h("div", { part: "sub" }, sub) : null,
3874
- encrypted ? h("div", { part: "sub" }, this.t("destination.encrypted")) : null
3892
+ h(
3893
+ "div",
3894
+ { part: "title" },
3895
+ title,
3896
+ // Only where there IS a backup: "Active" over the device-only
3897
+ // state would badge the absence of one as a live destination.
3898
+ durable ? h("span", { part: "tag destination-active" }, this.t("destination.active")) : null,
3899
+ encrypted ? h("span", { part: "tag destination-encrypted" }, this.t("destination.encrypted")) : null
3900
+ ),
3901
+ line ? h("div", { part: "sub" }, line) : null
3875
3902
  )
3876
3903
  ),
3877
- status,
3878
3904
  h(
3879
3905
  "div",
3880
3906
  { part: "destination-actions" },
@@ -3886,10 +3912,12 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3886
3912
  "destination-restore",
3887
3913
  () => this.emit("selfstore-destination-action", { action: "restore" })
3888
3914
  ),
3889
- this.#targets ? act("destination.change", "destination-change", () => {
3890
- this.#changing = true;
3891
- this.rerender();
3892
- }) : null,
3915
+ // Changing backup lets the destination go and stops there: the
3916
+ // first-run screen the app already mounts is what asks where to go
3917
+ // next. A second connect journey inside this panel gave the same
3918
+ // decision two mechanics depending on which control was pressed, and
3919
+ // only one of them could be reached from anywhere in the app.
3920
+ this.#targets ? act("destination.change", "destination-change", () => void this.detach(label)) : null,
3893
3921
  act("destination.detach", "destination-detach", () => void this.detach(label))
3894
3922
  ),
3895
3923
  this.#message ? h("div", { part: "sub destination-message", role: "status" }, this.#message) : null
@@ -4036,7 +4064,7 @@ var SelfstoreStorageElement = class extends FlowWidget {
4036
4064
 
4037
4065
  // src/widgets/account.ts
4038
4066
  var EN9 = {
4039
- ...EN7,
4067
+ ...EN4,
4040
4068
  "account.open": "Storage",
4041
4069
  "account.settings": "Settings",
4042
4070
  "account.change": "Change backup",
@@ -4044,7 +4072,7 @@ var EN9 = {
4044
4072
  "account.justNow": "just now"
4045
4073
  };
4046
4074
  var FR9 = {
4047
- ...FR7,
4075
+ ...FR4,
4048
4076
  "account.open": "Sauvegarde",
4049
4077
  "account.settings": "Param\xE8tres",
4050
4078
  "account.change": "Changer de sauvegarde",
@@ -4088,7 +4116,8 @@ var ACCOUNT_STYLES = `
4088
4116
  right: 0;
4089
4117
  top: calc(100% + 0.4rem);
4090
4118
  z-index: 20;
4091
- min-width: min(20rem, calc(100vw - 2rem));
4119
+ min-width: min(17rem, calc(100vw - 2rem));
4120
+ max-width: min(24rem, calc(100vw - 2rem));
4092
4121
  display: flex;
4093
4122
  flex-direction: column;
4094
4123
  align-items: stretch;
@@ -4113,7 +4142,7 @@ var ACCOUNT_STYLES = `
4113
4142
  cursor: pointer;
4114
4143
  }
4115
4144
  [part~='account-card']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
4116
- [part~='account-logo'] { width: 1.5rem; height: 1.5rem; flex: none; }
4145
+ [part~='account-logo'] { width: 1.4rem; height: 1.4rem; object-fit: contain; flex: none; }
4117
4146
  [part~='account-text'] {
4118
4147
  display: flex;
4119
4148
  flex-direction: column;
@@ -4121,26 +4150,47 @@ var ACCOUNT_STYLES = `
4121
4150
  min-width: 0;
4122
4151
  line-height: 1.35;
4123
4152
  }
4124
- [part~='account-title'] { font-weight: 600; }
4153
+ [part~='account-title'] {
4154
+ font-weight: 600;
4155
+ overflow: hidden;
4156
+ text-overflow: ellipsis;
4157
+ white-space: nowrap;
4158
+ }
4159
+ /* An address is long and its END is the part that identifies it, so it gets a
4160
+ line of its own and an ellipsis rather than a wrap that pushes the status
4161
+ line out of the card. */
4125
4162
  [part~='account-mail'], [part~='account-line'] {
4126
- font-size: 0.875rem;
4163
+ font-size: 0.8125rem;
4127
4164
  color: var(--_ink-dim);
4128
- overflow-wrap: anywhere;
4165
+ overflow: hidden;
4166
+ text-overflow: ellipsis;
4167
+ white-space: nowrap;
4129
4168
  }
4169
+ /* A menu item is a plain line of text, not a link: underlined blue links in a
4170
+ menu read as four different destinations to weigh, which is exactly the
4171
+ feeling this menu exists to remove. */
4130
4172
  [part~='account-item'] {
4131
4173
  text-align: start;
4132
- padding: 0.4rem 0.6rem;
4174
+ padding: 0.5rem 0.55rem;
4175
+ border: none;
4176
+ background: none;
4133
4177
  border-radius: calc(var(--_radius) * 0.75);
4178
+ font: inherit;
4179
+ font-size: 0.8125rem;
4134
4180
  text-decoration: none;
4135
4181
  color: inherit;
4182
+ cursor: pointer;
4183
+ white-space: nowrap;
4136
4184
  }
4137
4185
  [part~='account-item']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
4186
+ /* Letting a destination go is the one gesture here that takes something away:
4187
+ it wears the colour every other screen uses for that, and nothing else does. */
4188
+ [part~='account-change'] { color: var(--_danger); }
4189
+ [part~='account-change']:hover { background: color-mix(in srgb, var(--_danger) 10%, transparent); }
4138
4190
  [part~='account-sep'] { height: 1px; background: var(--_border); margin: 0.2rem 0.3rem; }
4139
4191
  `;
4140
4192
  var TICK_MS = 3e4;
4141
4193
  var SelfstoreAccountElement = class extends FlowWidget {
4142
- #store = null;
4143
- #icons = {};
4144
4194
  #confirm = null;
4145
4195
  #open = false;
4146
4196
  #timer = null;
@@ -4155,35 +4205,16 @@ var SelfstoreAccountElement = class extends FlowWidget {
4155
4205
  super();
4156
4206
  this.root.append(h("style", {}, ACCOUNT_STYLES));
4157
4207
  }
4158
- /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost.
4159
- * A simple store also answers `account` on its own, so nothing else is
4160
- * needed to name who holds the backup. */
4161
- get store() {
4162
- return this.#store;
4163
- }
4164
- set store(v) {
4165
- if (v === this.#store) return;
4166
- this.#store = v;
4167
- this.follow(hostOf(v));
4168
- }
4169
4208
  /** Who holds the backup ("someone@example.com"). Absent: whatever the store
4170
4209
  * knows. A brand name is not an address, and several accounts look alike. */
4171
4210
  #account = null;
4172
4211
  get account() {
4173
- return this.#account ?? this.#store?.account ?? null;
4212
+ return this.#account ?? this.store?.account ?? null;
4174
4213
  }
4175
4214
  set account(v) {
4176
4215
  this.#account = v || null;
4177
4216
  this.rerender();
4178
4217
  }
4179
- /** Optional glyph per destination kind, same contract as the connect cards. */
4180
- get icons() {
4181
- return this.#icons;
4182
- }
4183
- set icons(v) {
4184
- this.#icons = v ?? {};
4185
- this.rerender();
4186
- }
4187
4218
  /** Veto on changing backup, which leaves the app with no destination until
4188
4219
  * the user picks one. Returns false (or throws) to stop. */
4189
4220
  get confirmAction() {
@@ -4205,7 +4236,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4205
4236
  }
4206
4237
  connectedCallback() {
4207
4238
  super.connectedCallback();
4208
- this.follow(hostOf(this.#store));
4239
+ this.follow(this.hostOf());
4209
4240
  }
4210
4241
  disconnectedCallback() {
4211
4242
  this.unsub?.();
@@ -4245,17 +4276,6 @@ var SelfstoreAccountElement = class extends FlowWidget {
4245
4276
  const kind = this.t(`destination.kind.${targetKind}`);
4246
4277
  return kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
4247
4278
  }
4248
- /** "just now", "3 minutes ago", "yesterday" - in the page's language, from
4249
- * Intl rather than from five label keys per language. */
4250
- when(ts) {
4251
- const mins = Math.floor((Date.now() - ts) / 6e4);
4252
- if (mins < 1) return this.t("account.justNow");
4253
- const rtf = new Intl.RelativeTimeFormat(this.langTag(), { numeric: "auto" });
4254
- if (mins < 60) return rtf.format(-mins, "minute");
4255
- const hours = Math.floor(mins / 60);
4256
- if (hours < 24) return rtf.format(-hours, "hour");
4257
- return new Date(ts).toLocaleDateString(this.langTag());
4258
- }
4259
4279
  /**
4260
4280
  * The one line under the account. A plain "saved" is the only state this
4261
4281
  * element words itself, because it is the only one with nothing to do about
@@ -4269,15 +4289,15 @@ var SelfstoreAccountElement = class extends FlowWidget {
4269
4289
  return h(
4270
4290
  "span",
4271
4291
  { part: "account-line" },
4272
- this.t("account.saved", { when: this.when(lastSavedAt) })
4292
+ this.t("account.saved", { when: this.since(lastSavedAt, this.t("account.justNow")) })
4273
4293
  );
4274
4294
  const el = document.createElement(
4275
4295
  siblingTag(this.localName, "account", "status")
4276
4296
  );
4277
4297
  el.setAttribute("variant", "row");
4278
4298
  el.labels = this.labels;
4279
- el.icons = this.#icons;
4280
- el.store = this.#store;
4299
+ el.icons = this.icons;
4300
+ el.store = this.store;
4281
4301
  return el;
4282
4302
  }
4283
4303
  /** Ask for the settings. The host owns the route, so this only says that it
@@ -4290,7 +4310,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4290
4310
  /** Change backup: detach, and let the first-run screen take it from there.
4291
4311
  * The data stays; only the destination is let go. */
4292
4312
  async change() {
4293
- const host = hostOf(this.#store);
4313
+ const host = this.hostOf();
4294
4314
  if (!host) return;
4295
4315
  this.open = false;
4296
4316
  const label = host.engine.state.label;
@@ -4303,7 +4323,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4303
4323
  }
4304
4324
  card(host) {
4305
4325
  const { targetKind } = host.engine.state;
4306
- const icon = this.#icons[targetKind];
4326
+ const icon = this.icons[targetKind];
4307
4327
  const mail = this.account;
4308
4328
  return h(
4309
4329
  "button",
@@ -4325,7 +4345,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4325
4345
  );
4326
4346
  }
4327
4347
  view(into) {
4328
- const host = hostOf(this.#store);
4348
+ const host = this.hostOf();
4329
4349
  if (!host) return;
4330
4350
  const { status } = host.engine.state;
4331
4351
  const trigger = h(
@@ -4351,7 +4371,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4351
4371
  h(
4352
4372
  "button",
4353
4373
  {
4354
- part: "link account-item",
4374
+ part: "account-item",
4355
4375
  type: "button",
4356
4376
  role: "menuitem",
4357
4377
  onclick: () => this.settings()
@@ -4361,7 +4381,7 @@ var SelfstoreAccountElement = class extends FlowWidget {
4361
4381
  h(
4362
4382
  "button",
4363
4383
  {
4364
- part: "link account-item account-change",
4384
+ part: "account-item account-change",
4365
4385
  type: "button",
4366
4386
  role: "menuitem",
4367
4387
  onclick: () => void this.change()