selfstore 1.8.10 → 1.8.11

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
@@ -354,6 +354,10 @@ button[part~='menu-item-danger'] { color: var(--_danger); }
354
354
  }
355
355
  }
356
356
  `;
357
+ function siblingTag(own, self, name) {
358
+ const suffix = `-${self}`;
359
+ return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
360
+ }
357
361
  function put(el, ...children) {
358
362
  for (const c of children) {
359
363
  if (c == null) continue;
@@ -381,12 +385,15 @@ function h(tag, attrs = {}, ...children) {
381
385
  function upgradeOwnProperties(el) {
382
386
  const proto = Object.getPrototypeOf(el);
383
387
  const bag = el;
388
+ const claimed = /* @__PURE__ */ new Set();
384
389
  for (const name of Object.getOwnPropertyNames(el)) {
385
390
  if (!(name in proto)) continue;
386
391
  const value = bag[name];
387
392
  delete bag[name];
388
393
  bag[name] = value;
394
+ claimed.add(name);
389
395
  }
396
+ return claimed;
390
397
  }
391
398
  var FlowWidget = class extends HTMLElement {
392
399
  root;
@@ -426,13 +433,17 @@ var FlowWidget = class extends HTMLElement {
426
433
  this.overrides = map ?? {};
427
434
  this.rerender();
428
435
  }
429
- /** The shipped translation for the page's language, if the widget has one.
430
- * An explicit `lang` on the element (or on any ancestor) wins over the
431
- * document's, which wins over the browser's, so a host that already sets
432
- * `<html lang>` gets translated copy without writing a single label. */
436
+ /** The language this widget speaks. An explicit `lang` on the element (or on
437
+ * any ancestor) wins over the document's, which wins over the browser's, so
438
+ * a host that already sets `<html lang>` gets translated copy without
439
+ * writing a single label. Also what to hand `Intl` when a widget formats a
440
+ * date or a duration itself. */
441
+ langTag() {
442
+ return this.closest("[lang]")?.getAttribute("lang") || document.documentElement.lang || navigator.language || "en";
443
+ }
444
+ /** The shipped translation for the page's language, if the widget has one. */
433
445
  localized() {
434
- const tag = this.closest("[lang]")?.getAttribute("lang") || document.documentElement.lang || navigator.language || "";
435
- return this.packs()[tag.slice(0, 2).toLowerCase()] ?? {};
446
+ return this.packs()[this.langTag().slice(0, 2).toLowerCase()] ?? {};
436
447
  }
437
448
  /**
438
449
  * The copy for a key, with `{placeholders}` filled in.
@@ -468,8 +479,40 @@ var FlowWidget = class extends HTMLElement {
468
479
  /** Hands back whatever the host set before this class existed, then the
469
480
  * subclass reads its attributes and wires itself. Subclasses override this
470
481
  * and call `super.connectedCallback()` FIRST, for that reason. */
482
+ /**
483
+ * What the HOST decided before this element could hear it.
484
+ *
485
+ * Assigning a property to an element that has not upgraded yet is what every
486
+ * framework does - the widget bundle is loaded lazily, the assignment is
487
+ * not - and `upgradeOwnProperties` replays those assignments through the
488
+ * real setters. Then reading the same knob back from the markup would undo
489
+ * them: an attribute is a DEFAULT written by the page, a property is a
490
+ * decision taken by the app, and the decision has to win. A widget reads its
491
+ * attributes through this set.
492
+ */
493
+ assigned = /* @__PURE__ */ new Set();
471
494
  connectedCallback() {
472
- upgradeOwnProperties(this);
495
+ for (const name of upgradeOwnProperties(this)) this.assigned.add(name);
496
+ }
497
+ /**
498
+ * An attribute the host has NOT already decided in code, or null.
499
+ *
500
+ * Two ways it can have decided, and both have to be caught. It replayed
501
+ * through `assigned` on connect - or it has not been replayed yet, and its
502
+ * value is sitting on the element as an own property shadowing this very
503
+ * accessor. That second case is the one that bit: on upgrade the browser
504
+ * fires attributeChangedCallback BEFORE connectedCallback, so a widget
505
+ * writing the attribute through its own setter wrote into the shadowing own
506
+ * property instead - overwriting the decision it was about to replay.
507
+ */
508
+ attr(name) {
509
+ if (this.assigned.has(name) || Object.hasOwn(this, name)) return null;
510
+ return this.getAttribute(name);
511
+ }
512
+ /** The same, as a boolean: absent is null, anything but "false" is true. */
513
+ flag(name) {
514
+ const value = this.attr(name);
515
+ return value === null ? null : value !== "false";
473
516
  }
474
517
  emit(name, detail) {
475
518
  this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
@@ -3448,16 +3491,18 @@ var SelfstoreGateElement = class extends FlowWidget {
3448
3491
  super();
3449
3492
  this.root.append(h("style", {}, GATE_STYLES));
3450
3493
  }
3451
- attributeChangedCallback(name, _old, value) {
3452
- if (name === "armed") this.armed = value !== "false";
3453
- if (name === "deferrable") this.deferrable = value !== "false";
3494
+ attributeChangedCallback(name) {
3495
+ const value = this.flag(name);
3496
+ if (value === null) return;
3497
+ if (name === "armed") this.armed = value;
3498
+ if (name === "deferrable") this.deferrable = value;
3454
3499
  }
3455
3500
  connectedCallback() {
3456
3501
  super.connectedCallback();
3457
- if (this.hasAttribute("armed")) this.armed = this.getAttribute("armed") !== "false";
3458
- if (this.hasAttribute("deferrable")) {
3459
- this.deferrable = this.getAttribute("deferrable") !== "false";
3460
- }
3502
+ const armed = this.flag("armed");
3503
+ if (armed !== null) this.armed = armed;
3504
+ const deferrable = this.flag("deferrable");
3505
+ if (deferrable !== null) this.deferrable = deferrable;
3461
3506
  this.wire();
3462
3507
  }
3463
3508
  host() {
@@ -3583,14 +3628,25 @@ var SelfstoreGateElement = class extends FlowWidget {
3583
3628
  }
3584
3629
  };
3585
3630
 
3586
- // src/widgets/destination.ts
3631
+ // src/widgets/kinds.ts
3587
3632
  var EN7 = {
3588
- "destination.heading": "Where your data is kept",
3589
3633
  "destination.kind.drive": "Google Drive",
3590
3634
  "destination.kind.file": "A file on this device",
3591
3635
  "destination.kind.webdav": "Your own server",
3592
3636
  "destination.kind.s3": "Your own bucket",
3593
- "destination.kind.device": "This device only",
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
+ // src/widgets/destination.ts
3647
+ var EN8 = {
3648
+ ...EN7,
3649
+ "destination.heading": "Where your data is kept",
3594
3650
  "destination.encrypted": "Password-protected",
3595
3651
  "destination.export": "Export a copy",
3596
3652
  "destination.exported": "The copy is written.",
@@ -3602,12 +3658,9 @@ var EN7 = {
3602
3658
  "destination.back": "Back",
3603
3659
  "error.generic": "Something went wrong."
3604
3660
  };
3605
- var FR7 = {
3661
+ var FR8 = {
3662
+ ...FR7,
3606
3663
  "destination.heading": "O\xF9 vos donn\xE9es sont enregistr\xE9es",
3607
- "destination.kind.file": "Un fichier sur cet appareil",
3608
- "destination.kind.webdav": "Votre serveur",
3609
- "destination.kind.s3": "Votre bucket",
3610
- "destination.kind.device": "Cet appareil seulement",
3611
3664
  "destination.encrypted": "Prot\xE9g\xE9e par un mot de passe",
3612
3665
  "destination.export": "Exporter une copie",
3613
3666
  "destination.exported": "La copie est \xE9crite.",
@@ -3635,10 +3688,6 @@ var DESTINATION_STYLES = `
3635
3688
  font-weight: 600;
3636
3689
  }
3637
3690
  `;
3638
- function siblingTag(own, name) {
3639
- const suffix = "-destination";
3640
- return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
3641
- }
3642
3691
  var SelfstoreDestinationElement = class extends FlowWidget {
3643
3692
  #store = null;
3644
3693
  #targets = null;
@@ -3649,10 +3698,10 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3649
3698
  #busy = false;
3650
3699
  #message = null;
3651
3700
  defaults() {
3652
- return EN7;
3701
+ return EN8;
3653
3702
  }
3654
3703
  packs() {
3655
- return { fr: FR7 };
3704
+ return { fr: FR8 };
3656
3705
  }
3657
3706
  constructor() {
3658
3707
  super();
@@ -3763,7 +3812,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3763
3812
  * tested ones rather than a second implementation. */
3764
3813
  connectView(into) {
3765
3814
  const el = document.createElement(
3766
- siblingTag(this.localName, "connect")
3815
+ siblingTag(this.localName, "destination", "connect")
3767
3816
  );
3768
3817
  el.store = this.#store;
3769
3818
  el.targets = this.#targets;
@@ -3794,7 +3843,9 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3794
3843
  const title = kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
3795
3844
  const sub = [this.#account, label && label !== title ? label : null].filter(Boolean).join(" \xB7 ");
3796
3845
  const icon = this.#icons[targetKind];
3797
- const status = document.createElement(siblingTag(this.localName, "status"));
3846
+ const status = document.createElement(
3847
+ siblingTag(this.localName, "destination", "status")
3848
+ );
3798
3849
  status.setAttribute("variant", "row");
3799
3850
  status.labels = this.labels;
3800
3851
  status.icons = this.#icons;
@@ -3847,10 +3898,6 @@ var SelfstoreDestinationElement = class extends FlowWidget {
3847
3898
  };
3848
3899
 
3849
3900
  // src/widgets/storage.ts
3850
- function siblingTag2(own, name) {
3851
- const suffix = "-storage";
3852
- return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
3853
- }
3854
3901
  var SelfstoreStorageElement = class extends FlowWidget {
3855
3902
  static get observedAttributes() {
3856
3903
  return ["deferrable", "recommended"];
@@ -3928,16 +3975,22 @@ var SelfstoreStorageElement = class extends FlowWidget {
3928
3975
  this.#recommended = v;
3929
3976
  this.rerender();
3930
3977
  }
3931
- attributeChangedCallback(name, _old, value) {
3932
- if (name === "deferrable") this.deferrable = value !== "false";
3933
- if (name === "recommended") this.recommended = value;
3978
+ attributeChangedCallback(name) {
3979
+ if (name === "deferrable") {
3980
+ const value = this.flag(name);
3981
+ if (value !== null) this.deferrable = value;
3982
+ }
3983
+ if (name === "recommended") {
3984
+ const value = this.attr(name);
3985
+ if (value !== null) this.recommended = value;
3986
+ }
3934
3987
  }
3935
3988
  connectedCallback() {
3936
3989
  super.connectedCallback();
3937
- if (this.hasAttribute("deferrable"))
3938
- this.deferrable = this.getAttribute("deferrable") !== "false";
3939
- if (this.hasAttribute("recommended"))
3940
- this.recommended = this.getAttribute("recommended");
3990
+ const deferrable = this.flag("deferrable");
3991
+ if (deferrable !== null) this.deferrable = deferrable;
3992
+ const recommended = this.attr("recommended");
3993
+ if (recommended !== null) this.recommended = recommended;
3941
3994
  this.follow(hostOf(this.#store));
3942
3995
  }
3943
3996
  host() {
@@ -3955,7 +4008,7 @@ var SelfstoreStorageElement = class extends FlowWidget {
3955
4008
  const { ready, status } = host.engine.state;
3956
4009
  const wanted = !ready || status.action === "choose-destination" ? "gate" : "destination";
3957
4010
  if (this.#childKind !== wanted || !this.#child) {
3958
- this.#child = document.createElement(siblingTag2(this.localName, wanted));
4011
+ this.#child = document.createElement(siblingTag(this.localName, "storage", wanted));
3959
4012
  this.#childKind = wanted;
3960
4013
  }
3961
4014
  const el = this.#child;
@@ -3973,7 +4026,7 @@ var SelfstoreStorageElement = class extends FlowWidget {
3973
4026
  el.confirmAction = this.#confirm;
3974
4027
  }
3975
4028
  el.store = this.#store;
3976
- const list = wanted === "destination" && this.#manager ? Object.assign(document.createElement(siblingTag2(this.localName, "backups")), {
4029
+ const list = wanted === "destination" && this.#manager ? Object.assign(document.createElement(siblingTag(this.localName, "storage", "backups")), {
3977
4030
  labels: this.labels,
3978
4031
  manager: this.#manager
3979
4032
  }) : null;
@@ -3981,6 +4034,362 @@ var SelfstoreStorageElement = class extends FlowWidget {
3981
4034
  }
3982
4035
  };
3983
4036
 
4037
+ // src/widgets/account.ts
4038
+ var EN9 = {
4039
+ ...EN7,
4040
+ "account.open": "Storage",
4041
+ "account.settings": "Settings",
4042
+ "account.change": "Change backup",
4043
+ "account.saved": "Saved {when}",
4044
+ "account.justNow": "just now"
4045
+ };
4046
+ var FR9 = {
4047
+ ...FR7,
4048
+ "account.open": "Sauvegarde",
4049
+ "account.settings": "Param\xE8tres",
4050
+ "account.change": "Changer de sauvegarde",
4051
+ "account.saved": "Enregistr\xE9 {when}",
4052
+ "account.justNow": "\xE0 l'instant"
4053
+ };
4054
+ var ACCOUNT_STYLES = `
4055
+ :host { position: relative; display: inline-block; }
4056
+ /* Every other widget fills the width it is given, so the shared stack declares
4057
+ itself a size container (that is what lets a card lay out against ITS width
4058
+ and not the page's). This one is a control in a header: it must be as wide as
4059
+ its own label. Inline-size containment makes a box report an intrinsic width
4060
+ of ZERO - the pill collapsed to its dot in a flex header, with the label
4061
+ overflowing off screen. Nothing here queries its own width. */
4062
+ [part~='stack'] { container-type: normal; display: block; }
4063
+ [part~='account-trigger'] {
4064
+ display: inline-flex;
4065
+ align-items: center;
4066
+ gap: 0.4rem;
4067
+ max-width: 22rem;
4068
+ overflow: hidden;
4069
+ white-space: nowrap;
4070
+ text-overflow: ellipsis;
4071
+ font: inherit;
4072
+ color: inherit;
4073
+ background: transparent;
4074
+ border: 1px solid var(--_border);
4075
+ border-radius: 999px;
4076
+ padding: 0.3rem 0.7rem;
4077
+ cursor: pointer;
4078
+ }
4079
+ [part~='account-dot'] {
4080
+ width: 0.5rem;
4081
+ height: 0.5rem;
4082
+ border-radius: 50%;
4083
+ background: currentColor;
4084
+ flex: none;
4085
+ }
4086
+ [part~='account-menu'] {
4087
+ position: absolute;
4088
+ right: 0;
4089
+ top: calc(100% + 0.4rem);
4090
+ z-index: 20;
4091
+ min-width: min(20rem, calc(100vw - 2rem));
4092
+ display: flex;
4093
+ flex-direction: column;
4094
+ align-items: stretch;
4095
+ gap: 0.25rem;
4096
+ text-align: start;
4097
+ padding: 0.4rem;
4098
+ background: var(--selfstore-surface, Canvas);
4099
+ box-shadow: var(--selfstore-shadow, 0 10px 30px rgb(0 0 0 / 0.14));
4100
+ }
4101
+ [part~='account-card'] {
4102
+ display: flex;
4103
+ align-items: flex-start;
4104
+ gap: 0.6rem;
4105
+ width: 100%;
4106
+ font: inherit;
4107
+ color: inherit;
4108
+ text-align: start;
4109
+ background: transparent;
4110
+ border: none;
4111
+ border-radius: calc(var(--_radius) * 0.75);
4112
+ padding: 0.5rem 0.6rem;
4113
+ cursor: pointer;
4114
+ }
4115
+ [part~='account-card']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
4116
+ [part~='account-logo'] { width: 1.5rem; height: 1.5rem; flex: none; }
4117
+ [part~='account-text'] {
4118
+ display: flex;
4119
+ flex-direction: column;
4120
+ gap: 0.15rem;
4121
+ min-width: 0;
4122
+ line-height: 1.35;
4123
+ }
4124
+ [part~='account-title'] { font-weight: 600; }
4125
+ [part~='account-mail'], [part~='account-line'] {
4126
+ font-size: 0.875rem;
4127
+ color: var(--_ink-dim);
4128
+ overflow-wrap: anywhere;
4129
+ }
4130
+ [part~='account-item'] {
4131
+ text-align: start;
4132
+ padding: 0.4rem 0.6rem;
4133
+ border-radius: calc(var(--_radius) * 0.75);
4134
+ text-decoration: none;
4135
+ color: inherit;
4136
+ }
4137
+ [part~='account-item']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
4138
+ [part~='account-sep'] { height: 1px; background: var(--_border); margin: 0.2rem 0.3rem; }
4139
+ `;
4140
+ var TICK_MS = 3e4;
4141
+ var SelfstoreAccountElement = class extends FlowWidget {
4142
+ #store = null;
4143
+ #icons = {};
4144
+ #confirm = null;
4145
+ #open = false;
4146
+ #timer = null;
4147
+ #away = null;
4148
+ defaults() {
4149
+ return EN9;
4150
+ }
4151
+ packs() {
4152
+ return { fr: FR9 };
4153
+ }
4154
+ constructor() {
4155
+ super();
4156
+ this.root.append(h("style", {}, ACCOUNT_STYLES));
4157
+ }
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
+ /** Who holds the backup ("someone@example.com"). Absent: whatever the store
4170
+ * knows. A brand name is not an address, and several accounts look alike. */
4171
+ #account = null;
4172
+ get account() {
4173
+ return this.#account ?? this.#store?.account ?? null;
4174
+ }
4175
+ set account(v) {
4176
+ this.#account = v || null;
4177
+ this.rerender();
4178
+ }
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
+ /** Veto on changing backup, which leaves the app with no destination until
4188
+ * the user picks one. Returns false (or throws) to stop. */
4189
+ get confirmAction() {
4190
+ return this.#confirm;
4191
+ }
4192
+ set confirmAction(fn) {
4193
+ this.#confirm = fn;
4194
+ }
4195
+ /** Whether the menu is on screen. Settable, so a host can close it from its
4196
+ * own router after the settings item navigated away. */
4197
+ get open() {
4198
+ return this.#open;
4199
+ }
4200
+ set open(v) {
4201
+ if (v === this.#open) return;
4202
+ this.#open = v;
4203
+ this.watch();
4204
+ this.rerender();
4205
+ }
4206
+ connectedCallback() {
4207
+ super.connectedCallback();
4208
+ this.follow(hostOf(this.#store));
4209
+ }
4210
+ disconnectedCallback() {
4211
+ this.unsub?.();
4212
+ this.unsub = null;
4213
+ this.#open = false;
4214
+ this.watch();
4215
+ }
4216
+ /** While the menu is open: a clock for the relative time, and the two ways
4217
+ * out that make it a menu rather than a page - a click anywhere else, and
4218
+ * Escape. `pointerdown` and not `click`, so the gesture that closes it does
4219
+ * not also land on whatever was under the menu. Shadow DOM retargets the
4220
+ * event, so what is asked is the composed path, never `e.target`. */
4221
+ watch() {
4222
+ if (this.#timer) clearInterval(this.#timer);
4223
+ this.#timer = null;
4224
+ if (this.#away) {
4225
+ document.removeEventListener("pointerdown", this.#away, true);
4226
+ document.removeEventListener("keydown", this.#away, true);
4227
+ this.#away = null;
4228
+ }
4229
+ if (!this.#open || !this.isConnected) return;
4230
+ this.#timer = setInterval(() => this.rerender(), TICK_MS);
4231
+ this.#away = (e) => {
4232
+ if (e instanceof KeyboardEvent) {
4233
+ if (e.key === "Escape") this.open = false;
4234
+ return;
4235
+ }
4236
+ if (!e.composedPath().includes(this)) this.open = false;
4237
+ };
4238
+ document.addEventListener("pointerdown", this.#away, true);
4239
+ document.addEventListener("keydown", this.#away, true);
4240
+ }
4241
+ /** The destination's name. An unknown kind (a host's own target) has no
4242
+ * shipped name: its own label is a better answer than the raw key. */
4243
+ named(host) {
4244
+ const { targetKind, label } = host.engine.state;
4245
+ const kind = this.t(`destination.kind.${targetKind}`);
4246
+ return kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
4247
+ }
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
+ /**
4260
+ * The one line under the account. A plain "saved" is the only state this
4261
+ * element words itself, because it is the only one with nothing to do about
4262
+ * it - and the only one where WHEN is the whole information. Everything else
4263
+ * (reconnect, unlock, changes to download) belongs to <selfstore-status>,
4264
+ * which owns both the wording and the button that resolves it.
4265
+ */
4266
+ line(host) {
4267
+ const { status, lastSavedAt } = host.engine.state;
4268
+ if (status.state === "saved" && lastSavedAt && !("status.saved" in this.labels))
4269
+ return h(
4270
+ "span",
4271
+ { part: "account-line" },
4272
+ this.t("account.saved", { when: this.when(lastSavedAt) })
4273
+ );
4274
+ const el = document.createElement(
4275
+ siblingTag(this.localName, "account", "status")
4276
+ );
4277
+ el.setAttribute("variant", "row");
4278
+ el.labels = this.labels;
4279
+ el.icons = this.#icons;
4280
+ el.store = this.#store;
4281
+ return el;
4282
+ }
4283
+ /** Ask for the settings. The host owns the route, so this only says that it
4284
+ * was asked - and closes, because a menu that stays open behind a page
4285
+ * transition reads as a failed click. */
4286
+ settings() {
4287
+ this.open = false;
4288
+ this.emit("selfstore-account-settings");
4289
+ }
4290
+ /** Change backup: detach, and let the first-run screen take it from there.
4291
+ * The data stays; only the destination is let go. */
4292
+ async change() {
4293
+ const host = hostOf(this.#store);
4294
+ if (!host) return;
4295
+ this.open = false;
4296
+ const label = host.engine.state.label;
4297
+ try {
4298
+ if (this.#confirm && !await this.#confirm({ type: "detach", label })) return;
4299
+ } catch {
4300
+ return;
4301
+ }
4302
+ await host.engine.detachTarget();
4303
+ }
4304
+ card(host) {
4305
+ const { targetKind } = host.engine.state;
4306
+ const icon = this.#icons[targetKind];
4307
+ const mail = this.account;
4308
+ return h(
4309
+ "button",
4310
+ {
4311
+ part: "account-card",
4312
+ type: "button",
4313
+ role: "menuitem",
4314
+ title: this.t("account.settings"),
4315
+ onclick: () => this.settings()
4316
+ },
4317
+ icon ? h("img", { part: "account-logo", src: icon, alt: "" }) : null,
4318
+ h(
4319
+ "span",
4320
+ { part: "account-text" },
4321
+ h("span", { part: "account-title" }, this.named(host)),
4322
+ mail ? h("span", { part: "account-mail" }, mail) : null,
4323
+ this.line(host)
4324
+ )
4325
+ );
4326
+ }
4327
+ view(into) {
4328
+ const host = hostOf(this.#store);
4329
+ if (!host) return;
4330
+ const { status } = host.engine.state;
4331
+ const trigger = h(
4332
+ "button",
4333
+ {
4334
+ part: `account-trigger ${status.severity}`,
4335
+ type: "button",
4336
+ "aria-haspopup": "menu",
4337
+ "aria-expanded": String(this.#open),
4338
+ "aria-label": this.t("account.open"),
4339
+ onclick: () => this.open = !this.#open
4340
+ },
4341
+ // The severity colors the DOT, never the label: a whole line turning
4342
+ // amber reads as a warning about the destination's name.
4343
+ h("span", { part: `account-dot sev-${status.severity}` }),
4344
+ h("span", {}, this.named(host))
4345
+ );
4346
+ const menu = this.#open ? h(
4347
+ "div",
4348
+ { part: "card account-menu", role: "menu" },
4349
+ this.card(host),
4350
+ h("div", { part: "account-sep" }),
4351
+ h(
4352
+ "button",
4353
+ {
4354
+ part: "link account-item",
4355
+ type: "button",
4356
+ role: "menuitem",
4357
+ onclick: () => this.settings()
4358
+ },
4359
+ this.t("account.settings")
4360
+ ),
4361
+ h(
4362
+ "button",
4363
+ {
4364
+ part: "link account-item account-change",
4365
+ type: "button",
4366
+ role: "menuitem",
4367
+ onclick: () => void this.change()
4368
+ },
4369
+ this.t("account.change")
4370
+ )
4371
+ ) : null;
4372
+ put(into, trigger, menu);
4373
+ if (menu) this.anchor(menu);
4374
+ }
4375
+ /**
4376
+ * Which side the menu hangs from.
4377
+ *
4378
+ * A header control is usually at the right edge, so the menu hangs right -
4379
+ * and the moment the header wraps on a narrow window, that same control sits
4380
+ * at the LEFT edge and the menu hangs off the screen. Measured rather than
4381
+ * assumed: it hangs left when there is room to the right, and right
4382
+ * otherwise, which lands correctly at both edges without the host declaring
4383
+ * where it put the control.
4384
+ */
4385
+ anchor(menu) {
4386
+ const here = this.getBoundingClientRect();
4387
+ const fits = here.left + menu.getBoundingClientRect().width <= window.innerWidth - 8;
4388
+ menu.style.left = fits ? "0" : "auto";
4389
+ menu.style.right = fits ? "auto" : "0";
4390
+ }
4391
+ };
4392
+
3984
4393
  // src/entries/widgets.ts
3985
4394
  function defineSelfstoreWidgets(prefix = "selfstore") {
3986
4395
  const define = (name, ctor) => {
@@ -3994,8 +4403,9 @@ function defineSelfstoreWidgets(prefix = "selfstore") {
3994
4403
  define(`${prefix}-gate`, SelfstoreGateElement);
3995
4404
  define(`${prefix}-destination`, SelfstoreDestinationElement);
3996
4405
  define(`${prefix}-storage`, SelfstoreStorageElement);
4406
+ define(`${prefix}-account`, SelfstoreAccountElement);
3997
4407
  }
3998
4408
 
3999
- export { FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, SelfstoreStorageElement, defineSelfstoreWidgets };
4409
+ export { FlowWidget, SelfstoreAccountElement, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, SelfstoreStorageElement, defineSelfstoreWidgets };
4000
4410
  //# sourceMappingURL=widgets.js.map
4001
4411
  //# sourceMappingURL=widgets.js.map