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/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/widgets.d.ts +28 -33
- package/dist/widgets.js +203 -183
- package/dist/widgets.js.map +1 -1
- package/package.json +1 -1
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/
|
|
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
|
|
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
|
|
2117
|
+
return EN5;
|
|
2034
2118
|
}
|
|
2035
2119
|
packs() {
|
|
2036
|
-
return { fr:
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
2115
|
-
|
|
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" },
|
|
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
|
|
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
|
|
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
|
|
2380
|
+
return EN6;
|
|
2304
2381
|
}
|
|
2305
2382
|
packs() {
|
|
2306
|
-
return { fr:
|
|
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
|
|
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
|
|
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
|
|
3469
|
+
return EN7;
|
|
3393
3470
|
}
|
|
3394
3471
|
packs() {
|
|
3395
|
-
return { fr:
|
|
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
|
-
...
|
|
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
|
|
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
|
-
...
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
3845
|
-
const icon = this
|
|
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
|
|
3852
|
-
status.store = this
|
|
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(
|
|
3873
|
-
|
|
3874
|
-
|
|
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
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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(
|
|
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.
|
|
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'] {
|
|
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.
|
|
4163
|
+
font-size: 0.8125rem;
|
|
4127
4164
|
color: var(--_ink-dim);
|
|
4128
|
-
overflow
|
|
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.
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
4280
|
-
el.store = this
|
|
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(
|
|
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
|
|
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(
|
|
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: "
|
|
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: "
|
|
4384
|
+
part: "account-item account-change",
|
|
4365
4385
|
type: "button",
|
|
4366
4386
|
role: "menuitem",
|
|
4367
4387
|
onclick: () => void this.change()
|