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/README.md +1 -1
- package/dist/widgets.d.ts +127 -7
- package/dist/widgets.js +454 -44
- package/dist/widgets.js.map +1 -1
- package/package.json +1 -1
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
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
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
|
-
|
|
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
|
|
3452
|
-
|
|
3453
|
-
if (
|
|
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
|
-
|
|
3458
|
-
if (this.
|
|
3459
|
-
|
|
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/
|
|
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
|
|
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
|
|
3701
|
+
return EN8;
|
|
3653
3702
|
}
|
|
3654
3703
|
packs() {
|
|
3655
|
-
return { fr:
|
|
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(
|
|
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
|
|
3932
|
-
if (name === "deferrable")
|
|
3933
|
-
|
|
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
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
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(
|
|
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(
|
|
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
|