selfstore 1.8.10 → 1.8.12
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/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/widgets.d.ts +144 -25
- package/dist/widgets.js +575 -131
- 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%;
|
|
@@ -354,6 +370,10 @@ button[part~='menu-item-danger'] { color: var(--_danger); }
|
|
|
354
370
|
}
|
|
355
371
|
}
|
|
356
372
|
`;
|
|
373
|
+
function siblingTag(own, self, name) {
|
|
374
|
+
const suffix = `-${self}`;
|
|
375
|
+
return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
|
|
376
|
+
}
|
|
357
377
|
function put(el, ...children) {
|
|
358
378
|
for (const c of children) {
|
|
359
379
|
if (c == null) continue;
|
|
@@ -381,12 +401,15 @@ function h(tag, attrs = {}, ...children) {
|
|
|
381
401
|
function upgradeOwnProperties(el) {
|
|
382
402
|
const proto = Object.getPrototypeOf(el);
|
|
383
403
|
const bag = el;
|
|
404
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
384
405
|
for (const name of Object.getOwnPropertyNames(el)) {
|
|
385
406
|
if (!(name in proto)) continue;
|
|
386
407
|
const value = bag[name];
|
|
387
408
|
delete bag[name];
|
|
388
409
|
bag[name] = value;
|
|
410
|
+
claimed.add(name);
|
|
389
411
|
}
|
|
412
|
+
return claimed;
|
|
390
413
|
}
|
|
391
414
|
var FlowWidget = class extends HTMLElement {
|
|
392
415
|
root;
|
|
@@ -399,6 +422,42 @@ var FlowWidget = class extends HTMLElement {
|
|
|
399
422
|
packs() {
|
|
400
423
|
return {};
|
|
401
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
|
+
}
|
|
402
461
|
/** Follow an engine: rerender on every notification, replacing any previous
|
|
403
462
|
* subscription. A no-op before connection and on a null host, so a host may
|
|
404
463
|
* assign the store from a reactive effect that runs more than once. */
|
|
@@ -426,13 +485,17 @@ var FlowWidget = class extends HTMLElement {
|
|
|
426
485
|
this.overrides = map ?? {};
|
|
427
486
|
this.rerender();
|
|
428
487
|
}
|
|
429
|
-
/** The
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
488
|
+
/** The language this widget speaks. An explicit `lang` on the element (or on
|
|
489
|
+
* any ancestor) wins over the document's, which wins over the browser's, so
|
|
490
|
+
* a host that already sets `<html lang>` gets translated copy without
|
|
491
|
+
* writing a single label. Also what to hand `Intl` when a widget formats a
|
|
492
|
+
* date or a duration itself. */
|
|
493
|
+
langTag() {
|
|
494
|
+
return this.closest("[lang]")?.getAttribute("lang") || document.documentElement.lang || navigator.language || "en";
|
|
495
|
+
}
|
|
496
|
+
/** The shipped translation for the page's language, if the widget has one. */
|
|
433
497
|
localized() {
|
|
434
|
-
|
|
435
|
-
return this.packs()[tag.slice(0, 2).toLowerCase()] ?? {};
|
|
498
|
+
return this.packs()[this.langTag().slice(0, 2).toLowerCase()] ?? {};
|
|
436
499
|
}
|
|
437
500
|
/**
|
|
438
501
|
* The copy for a key, with `{placeholders}` filled in.
|
|
@@ -449,6 +512,21 @@ var FlowWidget = class extends HTMLElement {
|
|
|
449
512
|
if (!vars) return copy;
|
|
450
513
|
return copy.replace(/\{(\w+)\}/g, (whole, name) => vars[name] ?? whole);
|
|
451
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
|
+
}
|
|
452
530
|
/** A heading an empty-string label removes: labels = { 'share.title': '' }
|
|
453
531
|
* hides that heading (the host page already says it) without a fork.
|
|
454
532
|
* Append through put(), which skips the null. */
|
|
@@ -468,8 +546,40 @@ var FlowWidget = class extends HTMLElement {
|
|
|
468
546
|
/** Hands back whatever the host set before this class existed, then the
|
|
469
547
|
* subclass reads its attributes and wires itself. Subclasses override this
|
|
470
548
|
* and call `super.connectedCallback()` FIRST, for that reason. */
|
|
549
|
+
/**
|
|
550
|
+
* What the HOST decided before this element could hear it.
|
|
551
|
+
*
|
|
552
|
+
* Assigning a property to an element that has not upgraded yet is what every
|
|
553
|
+
* framework does - the widget bundle is loaded lazily, the assignment is
|
|
554
|
+
* not - and `upgradeOwnProperties` replays those assignments through the
|
|
555
|
+
* real setters. Then reading the same knob back from the markup would undo
|
|
556
|
+
* them: an attribute is a DEFAULT written by the page, a property is a
|
|
557
|
+
* decision taken by the app, and the decision has to win. A widget reads its
|
|
558
|
+
* attributes through this set.
|
|
559
|
+
*/
|
|
560
|
+
assigned = /* @__PURE__ */ new Set();
|
|
471
561
|
connectedCallback() {
|
|
472
|
-
upgradeOwnProperties(this);
|
|
562
|
+
for (const name of upgradeOwnProperties(this)) this.assigned.add(name);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* An attribute the host has NOT already decided in code, or null.
|
|
566
|
+
*
|
|
567
|
+
* Two ways it can have decided, and both have to be caught. It replayed
|
|
568
|
+
* through `assigned` on connect - or it has not been replayed yet, and its
|
|
569
|
+
* value is sitting on the element as an own property shadowing this very
|
|
570
|
+
* accessor. That second case is the one that bit: on upgrade the browser
|
|
571
|
+
* fires attributeChangedCallback BEFORE connectedCallback, so a widget
|
|
572
|
+
* writing the attribute through its own setter wrote into the shadowing own
|
|
573
|
+
* property instead - overwriting the decision it was about to replay.
|
|
574
|
+
*/
|
|
575
|
+
attr(name) {
|
|
576
|
+
if (this.assigned.has(name) || Object.hasOwn(this, name)) return null;
|
|
577
|
+
return this.getAttribute(name);
|
|
578
|
+
}
|
|
579
|
+
/** The same, as a boolean: absent is null, anything but "false" is true. */
|
|
580
|
+
flag(name) {
|
|
581
|
+
const value = this.attr(name);
|
|
582
|
+
return value === null ? null : value !== "false";
|
|
473
583
|
}
|
|
474
584
|
emit(name, detail) {
|
|
475
585
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
@@ -1950,8 +2060,25 @@ var SelfstoreJoinElement = class extends FlowWidget {
|
|
|
1950
2060
|
}
|
|
1951
2061
|
};
|
|
1952
2062
|
|
|
1953
|
-
// src/widgets/
|
|
2063
|
+
// src/widgets/kinds.ts
|
|
1954
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",
|
|
1955
2082
|
"status.ephemeral": "Nothing is saved",
|
|
1956
2083
|
"status.cacheOnly": "Never saved anywhere yet",
|
|
1957
2084
|
"status.saving": "Saving...",
|
|
@@ -1965,7 +2092,9 @@ var EN4 = {
|
|
|
1965
2092
|
"status.action.reconnect": "Reconnect",
|
|
1966
2093
|
"status.action.unlock": "Unlock"
|
|
1967
2094
|
};
|
|
1968
|
-
var
|
|
2095
|
+
var FR5 = {
|
|
2096
|
+
...FR4,
|
|
2097
|
+
"status.justNow": "a l'instant",
|
|
1969
2098
|
"status.ephemeral": "Rien n'est enregistr\xE9",
|
|
1970
2099
|
"status.cacheOnly": "Vos donn\xE9es n'ont jamais \xE9t\xE9 enregistr\xE9es ailleurs",
|
|
1971
2100
|
"status.saving": "Enregistrement...",
|
|
@@ -1983,23 +2112,12 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
1983
2112
|
static get observedAttributes() {
|
|
1984
2113
|
return ["variant"];
|
|
1985
2114
|
}
|
|
1986
|
-
#store = null;
|
|
1987
2115
|
#variant = "row";
|
|
1988
|
-
#icons = {};
|
|
1989
2116
|
defaults() {
|
|
1990
|
-
return
|
|
2117
|
+
return EN5;
|
|
1991
2118
|
}
|
|
1992
2119
|
packs() {
|
|
1993
|
-
return { fr:
|
|
1994
|
-
}
|
|
1995
|
-
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
1996
|
-
get store() {
|
|
1997
|
-
return this.#store;
|
|
1998
|
-
}
|
|
1999
|
-
set store(v) {
|
|
2000
|
-
if (v === this.#store) return;
|
|
2001
|
-
this.#store = v;
|
|
2002
|
-
this.wire();
|
|
2120
|
+
return { fr: FR5 };
|
|
2003
2121
|
}
|
|
2004
2122
|
/** 'row' (dot + text + action) or 'dot' (the dot alone). Also the attribute. */
|
|
2005
2123
|
get variant() {
|
|
@@ -2009,31 +2127,16 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
2009
2127
|
this.#variant = v === "dot" ? "dot" : "row";
|
|
2010
2128
|
this.rerender();
|
|
2011
2129
|
}
|
|
2012
|
-
/** Optional icon per target kind (e.g. { drive: url, file: url }), shown
|
|
2013
|
-
* before the text in the row variant. Same contract as the connect cards. */
|
|
2014
|
-
get icons() {
|
|
2015
|
-
return this.#icons;
|
|
2016
|
-
}
|
|
2017
|
-
set icons(v) {
|
|
2018
|
-
this.#icons = v ?? {};
|
|
2019
|
-
this.rerender();
|
|
2020
|
-
}
|
|
2021
2130
|
attributeChangedCallback(name, _old, value) {
|
|
2022
2131
|
if (name === "variant") this.variant = value;
|
|
2023
2132
|
}
|
|
2024
2133
|
connectedCallback() {
|
|
2025
2134
|
super.connectedCallback();
|
|
2026
2135
|
if (this.hasAttribute("variant")) this.variant = this.getAttribute("variant");
|
|
2027
|
-
this.
|
|
2028
|
-
}
|
|
2029
|
-
host() {
|
|
2030
|
-
return hostOf(this.#store);
|
|
2031
|
-
}
|
|
2032
|
-
wire() {
|
|
2033
|
-
this.follow(this.host());
|
|
2136
|
+
this.follow(this.hostOf());
|
|
2034
2137
|
}
|
|
2035
2138
|
view(into) {
|
|
2036
|
-
const host = this.
|
|
2139
|
+
const host = this.hostOf();
|
|
2037
2140
|
if (!host) return;
|
|
2038
2141
|
const { status, targetKind, label } = host.engine.state;
|
|
2039
2142
|
const sansLieu = (cle) => {
|
|
@@ -2041,6 +2144,7 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
2041
2144
|
const copie = this.t(jumelle);
|
|
2042
2145
|
return copie === jumelle ? this.t(cle) : copie;
|
|
2043
2146
|
};
|
|
2147
|
+
const { lastSavedAt } = host.engine.state;
|
|
2044
2148
|
const text = label ? this.t(status.labelKey, { label }) : sansLieu(status.labelKey);
|
|
2045
2149
|
const dot = h("span", {
|
|
2046
2150
|
part: `status-dot sev-${status.severity}`,
|
|
@@ -2061,20 +2165,36 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
2061
2165
|
);
|
|
2062
2166
|
return;
|
|
2063
2167
|
}
|
|
2064
|
-
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];
|
|
2065
2177
|
const where = this.t(`status.where.${targetKind}`);
|
|
2066
|
-
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;
|
|
2067
2185
|
put(
|
|
2068
2186
|
into,
|
|
2069
2187
|
h(
|
|
2070
2188
|
"div",
|
|
2071
|
-
{ part:
|
|
2072
|
-
|
|
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,
|
|
2073
2193
|
icon ? h("img", { part: "icon", src: icon, alt: "" }) : null,
|
|
2074
2194
|
h(
|
|
2075
2195
|
"div",
|
|
2076
2196
|
{},
|
|
2077
|
-
h("div", { part: "title" },
|
|
2197
|
+
h("div", { part: "title" }, title),
|
|
2078
2198
|
sub ? h("div", { part: "sub" }, sub) : null
|
|
2079
2199
|
),
|
|
2080
2200
|
status.actionable && status.action ? h(
|
|
@@ -2091,7 +2211,7 @@ var SelfstoreStatusElement = class extends FlowWidget {
|
|
|
2091
2211
|
};
|
|
2092
2212
|
|
|
2093
2213
|
// src/widgets/backups.ts
|
|
2094
|
-
var
|
|
2214
|
+
var EN6 = {
|
|
2095
2215
|
"backups.mine.heading": "My backups",
|
|
2096
2216
|
"backups.mine.personal": "Main backup",
|
|
2097
2217
|
"backups.active": "Active",
|
|
@@ -2148,7 +2268,7 @@ var EN5 = {
|
|
|
2148
2268
|
"backups.replica.s3.secret": "Secret access key",
|
|
2149
2269
|
"error.generic": "That did not work. Check the connection and try again."
|
|
2150
2270
|
};
|
|
2151
|
-
var
|
|
2271
|
+
var FR6 = {
|
|
2152
2272
|
"backups.mine.heading": "Sauvegardes",
|
|
2153
2273
|
"backups.mine.personal": "Sauvegarde principale",
|
|
2154
2274
|
"backups.active": "Active",
|
|
@@ -2257,10 +2377,10 @@ var SelfstoreBackupsElement = class extends FlowWidget {
|
|
|
2257
2377
|
#withShare = true;
|
|
2258
2378
|
#withShared = true;
|
|
2259
2379
|
defaults() {
|
|
2260
|
-
return
|
|
2380
|
+
return EN6;
|
|
2261
2381
|
}
|
|
2262
2382
|
packs() {
|
|
2263
|
-
return { fr:
|
|
2383
|
+
return { fr: FR6 };
|
|
2264
2384
|
}
|
|
2265
2385
|
/** The headless manager this panel drives (see 'selfstore/backups'). */
|
|
2266
2386
|
get manager() {
|
|
@@ -3275,14 +3395,14 @@ var SelfstoreBackupsElement = class extends FlowWidget {
|
|
|
3275
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)";
|
|
3276
3396
|
|
|
3277
3397
|
// src/widgets/gate.ts
|
|
3278
|
-
var
|
|
3398
|
+
var EN7 = {
|
|
3279
3399
|
"gate.title": "Where should your data live?",
|
|
3280
3400
|
"gate.hint": "It is encrypted on this device. Choose where to keep it and it gets saved there for you from now on.",
|
|
3281
3401
|
"gate.fine": "",
|
|
3282
3402
|
"gate.defer": "Later - keep it in this browser only",
|
|
3283
3403
|
"gate.defer.note": "Data kept only in this browser is lost if its storage is cleared."
|
|
3284
3404
|
};
|
|
3285
|
-
var
|
|
3405
|
+
var FR7 = {
|
|
3286
3406
|
"gate.title": "O\xF9 enregistrer vos donn\xE9es ?",
|
|
3287
3407
|
"gate.hint": "Elles sont chiffr\xE9es sur cet appareil. Choisissez une sauvegarde : tout y sera enregistr\xE9 automatiquement.",
|
|
3288
3408
|
"gate.fine": "",
|
|
@@ -3346,10 +3466,10 @@ var SelfstoreGateElement = class extends FlowWidget {
|
|
|
3346
3466
|
#open = false;
|
|
3347
3467
|
#connect = null;
|
|
3348
3468
|
defaults() {
|
|
3349
|
-
return
|
|
3469
|
+
return EN7;
|
|
3350
3470
|
}
|
|
3351
3471
|
packs() {
|
|
3352
|
-
return { fr:
|
|
3472
|
+
return { fr: FR7 };
|
|
3353
3473
|
}
|
|
3354
3474
|
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
3355
3475
|
get store() {
|
|
@@ -3448,16 +3568,18 @@ var SelfstoreGateElement = class extends FlowWidget {
|
|
|
3448
3568
|
super();
|
|
3449
3569
|
this.root.append(h("style", {}, GATE_STYLES));
|
|
3450
3570
|
}
|
|
3451
|
-
attributeChangedCallback(name
|
|
3452
|
-
|
|
3453
|
-
if (
|
|
3571
|
+
attributeChangedCallback(name) {
|
|
3572
|
+
const value = this.flag(name);
|
|
3573
|
+
if (value === null) return;
|
|
3574
|
+
if (name === "armed") this.armed = value;
|
|
3575
|
+
if (name === "deferrable") this.deferrable = value;
|
|
3454
3576
|
}
|
|
3455
3577
|
connectedCallback() {
|
|
3456
3578
|
super.connectedCallback();
|
|
3457
|
-
|
|
3458
|
-
if (this.
|
|
3459
|
-
|
|
3460
|
-
|
|
3579
|
+
const armed = this.flag("armed");
|
|
3580
|
+
if (armed !== null) this.armed = armed;
|
|
3581
|
+
const deferrable = this.flag("deferrable");
|
|
3582
|
+
if (deferrable !== null) this.deferrable = deferrable;
|
|
3461
3583
|
this.wire();
|
|
3462
3584
|
}
|
|
3463
3585
|
host() {
|
|
@@ -3584,14 +3706,13 @@ var SelfstoreGateElement = class extends FlowWidget {
|
|
|
3584
3706
|
};
|
|
3585
3707
|
|
|
3586
3708
|
// src/widgets/destination.ts
|
|
3587
|
-
var
|
|
3709
|
+
var EN8 = {
|
|
3710
|
+
...EN4,
|
|
3588
3711
|
"destination.heading": "Where your data is kept",
|
|
3589
|
-
"destination.kind.drive": "Google Drive",
|
|
3590
|
-
"destination.kind.file": "A file on this device",
|
|
3591
|
-
"destination.kind.webdav": "Your own server",
|
|
3592
|
-
"destination.kind.s3": "Your own bucket",
|
|
3593
|
-
"destination.kind.device": "This device only",
|
|
3594
3712
|
"destination.encrypted": "Password-protected",
|
|
3713
|
+
"destination.active": "Active",
|
|
3714
|
+
"destination.reach.drive": "On all your devices",
|
|
3715
|
+
"destination.reach.file": "",
|
|
3595
3716
|
"destination.export": "Export a copy",
|
|
3596
3717
|
"destination.exported": "The copy is written.",
|
|
3597
3718
|
"destination.exportCancelled": "Nothing was written.",
|
|
@@ -3602,13 +3723,13 @@ var EN7 = {
|
|
|
3602
3723
|
"destination.back": "Back",
|
|
3603
3724
|
"error.generic": "Something went wrong."
|
|
3604
3725
|
};
|
|
3605
|
-
var
|
|
3726
|
+
var FR8 = {
|
|
3727
|
+
...FR4,
|
|
3606
3728
|
"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
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": "",
|
|
3612
3733
|
"destination.export": "Exporter une copie",
|
|
3613
3734
|
"destination.exported": "La copie est \xE9crite.",
|
|
3614
3735
|
"destination.exportCancelled": "Rien n'a \xE9t\xE9 \xE9crit.",
|
|
@@ -3635,38 +3756,23 @@ var DESTINATION_STYLES = `
|
|
|
3635
3756
|
font-weight: 600;
|
|
3636
3757
|
}
|
|
3637
3758
|
`;
|
|
3638
|
-
function siblingTag(own, name) {
|
|
3639
|
-
const suffix = "-destination";
|
|
3640
|
-
return own.endsWith(suffix) ? `${own.slice(0, -suffix.length)}-${name}` : `selfstore-${name}`;
|
|
3641
|
-
}
|
|
3642
3759
|
var SelfstoreDestinationElement = class extends FlowWidget {
|
|
3643
|
-
#store = null;
|
|
3644
3760
|
#targets = null;
|
|
3645
3761
|
#account = null;
|
|
3646
|
-
#icons = {};
|
|
3647
3762
|
#confirm = null;
|
|
3648
3763
|
#changing = false;
|
|
3649
3764
|
#busy = false;
|
|
3650
3765
|
#message = null;
|
|
3651
3766
|
defaults() {
|
|
3652
|
-
return
|
|
3767
|
+
return EN8;
|
|
3653
3768
|
}
|
|
3654
3769
|
packs() {
|
|
3655
|
-
return { fr:
|
|
3770
|
+
return { fr: FR8 };
|
|
3656
3771
|
}
|
|
3657
3772
|
constructor() {
|
|
3658
3773
|
super();
|
|
3659
3774
|
this.root.append(h("style", {}, DESTINATION_STYLES));
|
|
3660
3775
|
}
|
|
3661
|
-
/** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
|
|
3662
|
-
get store() {
|
|
3663
|
-
return this.#store;
|
|
3664
|
-
}
|
|
3665
|
-
set store(v) {
|
|
3666
|
-
if (v === this.#store) return;
|
|
3667
|
-
this.#store = v;
|
|
3668
|
-
this.wire();
|
|
3669
|
-
}
|
|
3670
3776
|
/** The destinations offered when the user changes their mind. Given them, the
|
|
3671
3777
|
* panel runs the connect journey in place; without them the button is absent
|
|
3672
3778
|
* rather than dead - a host that cannot offer a choice should not show one. */
|
|
@@ -3687,14 +3793,6 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3687
3793
|
this.#account = v || null;
|
|
3688
3794
|
this.rerender();
|
|
3689
3795
|
}
|
|
3690
|
-
/** Optional glyph per destination kind, same contract as the connect cards. */
|
|
3691
|
-
get icons() {
|
|
3692
|
-
return this.#icons;
|
|
3693
|
-
}
|
|
3694
|
-
set icons(v) {
|
|
3695
|
-
this.#icons = v ?? {};
|
|
3696
|
-
this.rerender();
|
|
3697
|
-
}
|
|
3698
3796
|
/** Veto on what is destructive. Returns false (or throws) to stop; absent
|
|
3699
3797
|
* means proceed. The panel never invents its own confirm dialog: an app has
|
|
3700
3798
|
* its own words and its own modal. */
|
|
@@ -3706,13 +3804,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3706
3804
|
}
|
|
3707
3805
|
connectedCallback() {
|
|
3708
3806
|
super.connectedCallback();
|
|
3709
|
-
this.
|
|
3710
|
-
}
|
|
3711
|
-
host() {
|
|
3712
|
-
return hostOf(this.#store);
|
|
3713
|
-
}
|
|
3714
|
-
wire() {
|
|
3715
|
-
this.follow(this.host());
|
|
3807
|
+
this.follow(this.hostOf());
|
|
3716
3808
|
}
|
|
3717
3809
|
/** Run a gesture, keeping the panel honest while it is in flight. */
|
|
3718
3810
|
async run(job) {
|
|
@@ -3732,7 +3824,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3732
3824
|
* nothing, so it must not clear the pending-export nudge nor claim success -
|
|
3733
3825
|
* that is the download that lies about itself. */
|
|
3734
3826
|
exportCopy() {
|
|
3735
|
-
const host = this.
|
|
3827
|
+
const host = this.hostOf();
|
|
3736
3828
|
if (!host) return Promise.resolve();
|
|
3737
3829
|
return this.run(async () => {
|
|
3738
3830
|
const blob = await host.engine.exportBlob();
|
|
@@ -3745,7 +3837,7 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3745
3837
|
/** Stop saving to this destination. The local data stays, and so does the
|
|
3746
3838
|
* backup already written: this detaches, it never deletes. */
|
|
3747
3839
|
detach(label) {
|
|
3748
|
-
const host = this.
|
|
3840
|
+
const host = this.hostOf();
|
|
3749
3841
|
if (!host) return Promise.resolve();
|
|
3750
3842
|
return this.run(async () => {
|
|
3751
3843
|
try {
|
|
@@ -3763,11 +3855,11 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3763
3855
|
* tested ones rather than a second implementation. */
|
|
3764
3856
|
connectView(into) {
|
|
3765
3857
|
const el = document.createElement(
|
|
3766
|
-
siblingTag(this.localName, "connect")
|
|
3858
|
+
siblingTag(this.localName, "destination", "connect")
|
|
3767
3859
|
);
|
|
3768
|
-
el.store = this
|
|
3860
|
+
el.store = this.store;
|
|
3769
3861
|
el.targets = this.#targets;
|
|
3770
|
-
el.icons = this
|
|
3862
|
+
el.icons = this.icons;
|
|
3771
3863
|
el.labels = this.labels;
|
|
3772
3864
|
const done = () => {
|
|
3773
3865
|
this.#changing = false;
|
|
@@ -3786,19 +3878,21 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3786
3878
|
);
|
|
3787
3879
|
}
|
|
3788
3880
|
view(into) {
|
|
3789
|
-
const host = this.
|
|
3881
|
+
const host = this.hostOf();
|
|
3790
3882
|
if (!host) return;
|
|
3791
3883
|
if (this.#changing && this.#targets) return this.connectView(into);
|
|
3792
3884
|
const { targetKind, label, encrypted } = host.engine.state;
|
|
3793
3885
|
const kind = this.t(`destination.kind.${targetKind}`);
|
|
3794
3886
|
const title = kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
|
|
3795
|
-
const sub =
|
|
3796
|
-
const icon = this
|
|
3797
|
-
const status = document.createElement(
|
|
3887
|
+
const sub = this.#account || (label && label !== title ? label : null);
|
|
3888
|
+
const icon = this.icons[targetKind];
|
|
3889
|
+
const status = document.createElement(
|
|
3890
|
+
siblingTag(this.localName, "destination", "status")
|
|
3891
|
+
);
|
|
3798
3892
|
status.setAttribute("variant", "row");
|
|
3799
3893
|
status.labels = this.labels;
|
|
3800
|
-
status.icons = this
|
|
3801
|
-
status.store = this
|
|
3894
|
+
status.icons = this.icons;
|
|
3895
|
+
status.store = this.store;
|
|
3802
3896
|
const act = (key, part, onclick) => {
|
|
3803
3897
|
const label2 = this.t(key);
|
|
3804
3898
|
if (!label2) return null;
|
|
@@ -3808,8 +3902,14 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3808
3902
|
label2
|
|
3809
3903
|
);
|
|
3810
3904
|
};
|
|
3905
|
+
const durable = targetKind !== "device" && targetKind !== "ephemeral";
|
|
3906
|
+
const reach = this.t(`destination.reach.${targetKind}`);
|
|
3907
|
+
const line = [sub, reach === `destination.reach.${targetKind}` ? null : reach].filter(Boolean).join(" \xB7 ");
|
|
3811
3908
|
put(
|
|
3812
3909
|
into,
|
|
3910
|
+
// The situation first, then what holds it: a panel that opens on a card
|
|
3911
|
+
// makes the reader work out from the name whether anything is wrong.
|
|
3912
|
+
status,
|
|
3813
3913
|
this.heading("destination-heading", "destination.heading"),
|
|
3814
3914
|
h(
|
|
3815
3915
|
"div",
|
|
@@ -3818,12 +3918,18 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3818
3918
|
h(
|
|
3819
3919
|
"div",
|
|
3820
3920
|
{},
|
|
3821
|
-
h(
|
|
3822
|
-
|
|
3823
|
-
|
|
3921
|
+
h(
|
|
3922
|
+
"div",
|
|
3923
|
+
{ part: "title" },
|
|
3924
|
+
title,
|
|
3925
|
+
// Only where there IS a backup: "Active" over the device-only
|
|
3926
|
+
// state would badge the absence of one as a live destination.
|
|
3927
|
+
durable ? h("span", { part: "tag destination-active" }, this.t("destination.active")) : null,
|
|
3928
|
+
encrypted ? h("span", { part: "tag destination-encrypted" }, this.t("destination.encrypted")) : null
|
|
3929
|
+
),
|
|
3930
|
+
line ? h("div", { part: "sub" }, line) : null
|
|
3824
3931
|
)
|
|
3825
3932
|
),
|
|
3826
|
-
status,
|
|
3827
3933
|
h(
|
|
3828
3934
|
"div",
|
|
3829
3935
|
{ part: "destination-actions" },
|
|
@@ -3847,10 +3953,6 @@ var SelfstoreDestinationElement = class extends FlowWidget {
|
|
|
3847
3953
|
};
|
|
3848
3954
|
|
|
3849
3955
|
// 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
3956
|
var SelfstoreStorageElement = class extends FlowWidget {
|
|
3855
3957
|
static get observedAttributes() {
|
|
3856
3958
|
return ["deferrable", "recommended"];
|
|
@@ -3928,16 +4030,22 @@ var SelfstoreStorageElement = class extends FlowWidget {
|
|
|
3928
4030
|
this.#recommended = v;
|
|
3929
4031
|
this.rerender();
|
|
3930
4032
|
}
|
|
3931
|
-
attributeChangedCallback(name
|
|
3932
|
-
if (name === "deferrable")
|
|
3933
|
-
|
|
4033
|
+
attributeChangedCallback(name) {
|
|
4034
|
+
if (name === "deferrable") {
|
|
4035
|
+
const value = this.flag(name);
|
|
4036
|
+
if (value !== null) this.deferrable = value;
|
|
4037
|
+
}
|
|
4038
|
+
if (name === "recommended") {
|
|
4039
|
+
const value = this.attr(name);
|
|
4040
|
+
if (value !== null) this.recommended = value;
|
|
4041
|
+
}
|
|
3934
4042
|
}
|
|
3935
4043
|
connectedCallback() {
|
|
3936
4044
|
super.connectedCallback();
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
4045
|
+
const deferrable = this.flag("deferrable");
|
|
4046
|
+
if (deferrable !== null) this.deferrable = deferrable;
|
|
4047
|
+
const recommended = this.attr("recommended");
|
|
4048
|
+
if (recommended !== null) this.recommended = recommended;
|
|
3941
4049
|
this.follow(hostOf(this.#store));
|
|
3942
4050
|
}
|
|
3943
4051
|
host() {
|
|
@@ -3955,7 +4063,7 @@ var SelfstoreStorageElement = class extends FlowWidget {
|
|
|
3955
4063
|
const { ready, status } = host.engine.state;
|
|
3956
4064
|
const wanted = !ready || status.action === "choose-destination" ? "gate" : "destination";
|
|
3957
4065
|
if (this.#childKind !== wanted || !this.#child) {
|
|
3958
|
-
this.#child = document.createElement(
|
|
4066
|
+
this.#child = document.createElement(siblingTag(this.localName, "storage", wanted));
|
|
3959
4067
|
this.#childKind = wanted;
|
|
3960
4068
|
}
|
|
3961
4069
|
const el = this.#child;
|
|
@@ -3973,7 +4081,7 @@ var SelfstoreStorageElement = class extends FlowWidget {
|
|
|
3973
4081
|
el.confirmAction = this.#confirm;
|
|
3974
4082
|
}
|
|
3975
4083
|
el.store = this.#store;
|
|
3976
|
-
const list = wanted === "destination" && this.#manager ? Object.assign(document.createElement(
|
|
4084
|
+
const list = wanted === "destination" && this.#manager ? Object.assign(document.createElement(siblingTag(this.localName, "storage", "backups")), {
|
|
3977
4085
|
labels: this.labels,
|
|
3978
4086
|
manager: this.#manager
|
|
3979
4087
|
}) : null;
|
|
@@ -3981,6 +4089,341 @@ var SelfstoreStorageElement = class extends FlowWidget {
|
|
|
3981
4089
|
}
|
|
3982
4090
|
};
|
|
3983
4091
|
|
|
4092
|
+
// src/widgets/account.ts
|
|
4093
|
+
var EN9 = {
|
|
4094
|
+
...EN4,
|
|
4095
|
+
"account.open": "Storage",
|
|
4096
|
+
"account.settings": "Settings",
|
|
4097
|
+
"account.change": "Change backup",
|
|
4098
|
+
"account.saved": "Saved {when}",
|
|
4099
|
+
"account.justNow": "just now"
|
|
4100
|
+
};
|
|
4101
|
+
var FR9 = {
|
|
4102
|
+
...FR4,
|
|
4103
|
+
"account.open": "Sauvegarde",
|
|
4104
|
+
"account.settings": "Param\xE8tres",
|
|
4105
|
+
"account.change": "Changer de sauvegarde",
|
|
4106
|
+
"account.saved": "Enregistr\xE9 {when}",
|
|
4107
|
+
"account.justNow": "\xE0 l'instant"
|
|
4108
|
+
};
|
|
4109
|
+
var ACCOUNT_STYLES = `
|
|
4110
|
+
:host { position: relative; display: inline-block; }
|
|
4111
|
+
/* Every other widget fills the width it is given, so the shared stack declares
|
|
4112
|
+
itself a size container (that is what lets a card lay out against ITS width
|
|
4113
|
+
and not the page's). This one is a control in a header: it must be as wide as
|
|
4114
|
+
its own label. Inline-size containment makes a box report an intrinsic width
|
|
4115
|
+
of ZERO - the pill collapsed to its dot in a flex header, with the label
|
|
4116
|
+
overflowing off screen. Nothing here queries its own width. */
|
|
4117
|
+
[part~='stack'] { container-type: normal; display: block; }
|
|
4118
|
+
[part~='account-trigger'] {
|
|
4119
|
+
display: inline-flex;
|
|
4120
|
+
align-items: center;
|
|
4121
|
+
gap: 0.4rem;
|
|
4122
|
+
max-width: 22rem;
|
|
4123
|
+
overflow: hidden;
|
|
4124
|
+
white-space: nowrap;
|
|
4125
|
+
text-overflow: ellipsis;
|
|
4126
|
+
font: inherit;
|
|
4127
|
+
color: inherit;
|
|
4128
|
+
background: transparent;
|
|
4129
|
+
border: 1px solid var(--_border);
|
|
4130
|
+
border-radius: 999px;
|
|
4131
|
+
padding: 0.3rem 0.7rem;
|
|
4132
|
+
cursor: pointer;
|
|
4133
|
+
}
|
|
4134
|
+
[part~='account-dot'] {
|
|
4135
|
+
width: 0.5rem;
|
|
4136
|
+
height: 0.5rem;
|
|
4137
|
+
border-radius: 50%;
|
|
4138
|
+
background: currentColor;
|
|
4139
|
+
flex: none;
|
|
4140
|
+
}
|
|
4141
|
+
[part~='account-menu'] {
|
|
4142
|
+
position: absolute;
|
|
4143
|
+
right: 0;
|
|
4144
|
+
top: calc(100% + 0.4rem);
|
|
4145
|
+
z-index: 20;
|
|
4146
|
+
min-width: min(17rem, calc(100vw - 2rem));
|
|
4147
|
+
max-width: min(24rem, calc(100vw - 2rem));
|
|
4148
|
+
display: flex;
|
|
4149
|
+
flex-direction: column;
|
|
4150
|
+
align-items: stretch;
|
|
4151
|
+
gap: 0.25rem;
|
|
4152
|
+
text-align: start;
|
|
4153
|
+
padding: 0.4rem;
|
|
4154
|
+
background: var(--selfstore-surface, Canvas);
|
|
4155
|
+
box-shadow: var(--selfstore-shadow, 0 10px 30px rgb(0 0 0 / 0.14));
|
|
4156
|
+
}
|
|
4157
|
+
[part~='account-card'] {
|
|
4158
|
+
display: flex;
|
|
4159
|
+
align-items: flex-start;
|
|
4160
|
+
gap: 0.6rem;
|
|
4161
|
+
width: 100%;
|
|
4162
|
+
font: inherit;
|
|
4163
|
+
color: inherit;
|
|
4164
|
+
text-align: start;
|
|
4165
|
+
background: transparent;
|
|
4166
|
+
border: none;
|
|
4167
|
+
border-radius: calc(var(--_radius) * 0.75);
|
|
4168
|
+
padding: 0.5rem 0.6rem;
|
|
4169
|
+
cursor: pointer;
|
|
4170
|
+
}
|
|
4171
|
+
[part~='account-card']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
|
|
4172
|
+
[part~='account-logo'] { width: 1.4rem; height: 1.4rem; object-fit: contain; flex: none; }
|
|
4173
|
+
[part~='account-text'] {
|
|
4174
|
+
display: flex;
|
|
4175
|
+
flex-direction: column;
|
|
4176
|
+
gap: 0.15rem;
|
|
4177
|
+
min-width: 0;
|
|
4178
|
+
line-height: 1.35;
|
|
4179
|
+
}
|
|
4180
|
+
[part~='account-title'] {
|
|
4181
|
+
font-weight: 600;
|
|
4182
|
+
overflow: hidden;
|
|
4183
|
+
text-overflow: ellipsis;
|
|
4184
|
+
white-space: nowrap;
|
|
4185
|
+
}
|
|
4186
|
+
/* An address is long and its END is the part that identifies it, so it gets a
|
|
4187
|
+
line of its own and an ellipsis rather than a wrap that pushes the status
|
|
4188
|
+
line out of the card. */
|
|
4189
|
+
[part~='account-mail'], [part~='account-line'] {
|
|
4190
|
+
font-size: 0.8125rem;
|
|
4191
|
+
color: var(--_ink-dim);
|
|
4192
|
+
overflow: hidden;
|
|
4193
|
+
text-overflow: ellipsis;
|
|
4194
|
+
white-space: nowrap;
|
|
4195
|
+
}
|
|
4196
|
+
[part~='account-item'] {
|
|
4197
|
+
text-align: start;
|
|
4198
|
+
padding: 0.4rem 0.6rem;
|
|
4199
|
+
border-radius: calc(var(--_radius) * 0.75);
|
|
4200
|
+
text-decoration: none;
|
|
4201
|
+
color: inherit;
|
|
4202
|
+
}
|
|
4203
|
+
[part~='account-item']:hover { background: color-mix(in srgb, currentColor 7%, transparent); }
|
|
4204
|
+
[part~='account-sep'] { height: 1px; background: var(--_border); margin: 0.2rem 0.3rem; }
|
|
4205
|
+
`;
|
|
4206
|
+
var TICK_MS = 3e4;
|
|
4207
|
+
var SelfstoreAccountElement = class extends FlowWidget {
|
|
4208
|
+
#confirm = null;
|
|
4209
|
+
#open = false;
|
|
4210
|
+
#timer = null;
|
|
4211
|
+
#away = null;
|
|
4212
|
+
defaults() {
|
|
4213
|
+
return EN9;
|
|
4214
|
+
}
|
|
4215
|
+
packs() {
|
|
4216
|
+
return { fr: FR9 };
|
|
4217
|
+
}
|
|
4218
|
+
constructor() {
|
|
4219
|
+
super();
|
|
4220
|
+
this.root.append(h("style", {}, ACCOUNT_STYLES));
|
|
4221
|
+
}
|
|
4222
|
+
/** Who holds the backup ("someone@example.com"). Absent: whatever the store
|
|
4223
|
+
* knows. A brand name is not an address, and several accounts look alike. */
|
|
4224
|
+
#account = null;
|
|
4225
|
+
get account() {
|
|
4226
|
+
return this.#account ?? this.store?.account ?? null;
|
|
4227
|
+
}
|
|
4228
|
+
set account(v) {
|
|
4229
|
+
this.#account = v || null;
|
|
4230
|
+
this.rerender();
|
|
4231
|
+
}
|
|
4232
|
+
/** Veto on changing backup, which leaves the app with no destination until
|
|
4233
|
+
* the user picks one. Returns false (or throws) to stop. */
|
|
4234
|
+
get confirmAction() {
|
|
4235
|
+
return this.#confirm;
|
|
4236
|
+
}
|
|
4237
|
+
set confirmAction(fn) {
|
|
4238
|
+
this.#confirm = fn;
|
|
4239
|
+
}
|
|
4240
|
+
/** Whether the menu is on screen. Settable, so a host can close it from its
|
|
4241
|
+
* own router after the settings item navigated away. */
|
|
4242
|
+
get open() {
|
|
4243
|
+
return this.#open;
|
|
4244
|
+
}
|
|
4245
|
+
set open(v) {
|
|
4246
|
+
if (v === this.#open) return;
|
|
4247
|
+
this.#open = v;
|
|
4248
|
+
this.watch();
|
|
4249
|
+
this.rerender();
|
|
4250
|
+
}
|
|
4251
|
+
connectedCallback() {
|
|
4252
|
+
super.connectedCallback();
|
|
4253
|
+
this.follow(this.hostOf());
|
|
4254
|
+
}
|
|
4255
|
+
disconnectedCallback() {
|
|
4256
|
+
this.unsub?.();
|
|
4257
|
+
this.unsub = null;
|
|
4258
|
+
this.#open = false;
|
|
4259
|
+
this.watch();
|
|
4260
|
+
}
|
|
4261
|
+
/** While the menu is open: a clock for the relative time, and the two ways
|
|
4262
|
+
* out that make it a menu rather than a page - a click anywhere else, and
|
|
4263
|
+
* Escape. `pointerdown` and not `click`, so the gesture that closes it does
|
|
4264
|
+
* not also land on whatever was under the menu. Shadow DOM retargets the
|
|
4265
|
+
* event, so what is asked is the composed path, never `e.target`. */
|
|
4266
|
+
watch() {
|
|
4267
|
+
if (this.#timer) clearInterval(this.#timer);
|
|
4268
|
+
this.#timer = null;
|
|
4269
|
+
if (this.#away) {
|
|
4270
|
+
document.removeEventListener("pointerdown", this.#away, true);
|
|
4271
|
+
document.removeEventListener("keydown", this.#away, true);
|
|
4272
|
+
this.#away = null;
|
|
4273
|
+
}
|
|
4274
|
+
if (!this.#open || !this.isConnected) return;
|
|
4275
|
+
this.#timer = setInterval(() => this.rerender(), TICK_MS);
|
|
4276
|
+
this.#away = (e) => {
|
|
4277
|
+
if (e instanceof KeyboardEvent) {
|
|
4278
|
+
if (e.key === "Escape") this.open = false;
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
if (!e.composedPath().includes(this)) this.open = false;
|
|
4282
|
+
};
|
|
4283
|
+
document.addEventListener("pointerdown", this.#away, true);
|
|
4284
|
+
document.addEventListener("keydown", this.#away, true);
|
|
4285
|
+
}
|
|
4286
|
+
/** The destination's name. An unknown kind (a host's own target) has no
|
|
4287
|
+
* shipped name: its own label is a better answer than the raw key. */
|
|
4288
|
+
named(host) {
|
|
4289
|
+
const { targetKind, label } = host.engine.state;
|
|
4290
|
+
const kind = this.t(`destination.kind.${targetKind}`);
|
|
4291
|
+
return kind === `destination.kind.${targetKind}` ? label ?? targetKind : kind;
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* The one line under the account. A plain "saved" is the only state this
|
|
4295
|
+
* element words itself, because it is the only one with nothing to do about
|
|
4296
|
+
* it - and the only one where WHEN is the whole information. Everything else
|
|
4297
|
+
* (reconnect, unlock, changes to download) belongs to <selfstore-status>,
|
|
4298
|
+
* which owns both the wording and the button that resolves it.
|
|
4299
|
+
*/
|
|
4300
|
+
line(host) {
|
|
4301
|
+
const { status, lastSavedAt } = host.engine.state;
|
|
4302
|
+
if (status.state === "saved" && lastSavedAt && !("status.saved" in this.labels))
|
|
4303
|
+
return h(
|
|
4304
|
+
"span",
|
|
4305
|
+
{ part: "account-line" },
|
|
4306
|
+
this.t("account.saved", { when: this.since(lastSavedAt, this.t("account.justNow")) })
|
|
4307
|
+
);
|
|
4308
|
+
const el = document.createElement(
|
|
4309
|
+
siblingTag(this.localName, "account", "status")
|
|
4310
|
+
);
|
|
4311
|
+
el.setAttribute("variant", "row");
|
|
4312
|
+
el.labels = this.labels;
|
|
4313
|
+
el.icons = this.icons;
|
|
4314
|
+
el.store = this.store;
|
|
4315
|
+
return el;
|
|
4316
|
+
}
|
|
4317
|
+
/** Ask for the settings. The host owns the route, so this only says that it
|
|
4318
|
+
* was asked - and closes, because a menu that stays open behind a page
|
|
4319
|
+
* transition reads as a failed click. */
|
|
4320
|
+
settings() {
|
|
4321
|
+
this.open = false;
|
|
4322
|
+
this.emit("selfstore-account-settings");
|
|
4323
|
+
}
|
|
4324
|
+
/** Change backup: detach, and let the first-run screen take it from there.
|
|
4325
|
+
* The data stays; only the destination is let go. */
|
|
4326
|
+
async change() {
|
|
4327
|
+
const host = this.hostOf();
|
|
4328
|
+
if (!host) return;
|
|
4329
|
+
this.open = false;
|
|
4330
|
+
const label = host.engine.state.label;
|
|
4331
|
+
try {
|
|
4332
|
+
if (this.#confirm && !await this.#confirm({ type: "detach", label })) return;
|
|
4333
|
+
} catch {
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
await host.engine.detachTarget();
|
|
4337
|
+
}
|
|
4338
|
+
card(host) {
|
|
4339
|
+
const { targetKind } = host.engine.state;
|
|
4340
|
+
const icon = this.icons[targetKind];
|
|
4341
|
+
const mail = this.account;
|
|
4342
|
+
return h(
|
|
4343
|
+
"button",
|
|
4344
|
+
{
|
|
4345
|
+
part: "account-card",
|
|
4346
|
+
type: "button",
|
|
4347
|
+
role: "menuitem",
|
|
4348
|
+
title: this.t("account.settings"),
|
|
4349
|
+
onclick: () => this.settings()
|
|
4350
|
+
},
|
|
4351
|
+
icon ? h("img", { part: "account-logo", src: icon, alt: "" }) : null,
|
|
4352
|
+
h(
|
|
4353
|
+
"span",
|
|
4354
|
+
{ part: "account-text" },
|
|
4355
|
+
h("span", { part: "account-title" }, this.named(host)),
|
|
4356
|
+
mail ? h("span", { part: "account-mail" }, mail) : null,
|
|
4357
|
+
this.line(host)
|
|
4358
|
+
)
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
view(into) {
|
|
4362
|
+
const host = this.hostOf();
|
|
4363
|
+
if (!host) return;
|
|
4364
|
+
const { status } = host.engine.state;
|
|
4365
|
+
const trigger = h(
|
|
4366
|
+
"button",
|
|
4367
|
+
{
|
|
4368
|
+
part: `account-trigger ${status.severity}`,
|
|
4369
|
+
type: "button",
|
|
4370
|
+
"aria-haspopup": "menu",
|
|
4371
|
+
"aria-expanded": String(this.#open),
|
|
4372
|
+
"aria-label": this.t("account.open"),
|
|
4373
|
+
onclick: () => this.open = !this.#open
|
|
4374
|
+
},
|
|
4375
|
+
// The severity colors the DOT, never the label: a whole line turning
|
|
4376
|
+
// amber reads as a warning about the destination's name.
|
|
4377
|
+
h("span", { part: `account-dot sev-${status.severity}` }),
|
|
4378
|
+
h("span", {}, this.named(host))
|
|
4379
|
+
);
|
|
4380
|
+
const menu = this.#open ? h(
|
|
4381
|
+
"div",
|
|
4382
|
+
{ part: "card account-menu", role: "menu" },
|
|
4383
|
+
this.card(host),
|
|
4384
|
+
h("div", { part: "account-sep" }),
|
|
4385
|
+
h(
|
|
4386
|
+
"button",
|
|
4387
|
+
{
|
|
4388
|
+
part: "link account-item",
|
|
4389
|
+
type: "button",
|
|
4390
|
+
role: "menuitem",
|
|
4391
|
+
onclick: () => this.settings()
|
|
4392
|
+
},
|
|
4393
|
+
this.t("account.settings")
|
|
4394
|
+
),
|
|
4395
|
+
h(
|
|
4396
|
+
"button",
|
|
4397
|
+
{
|
|
4398
|
+
part: "link account-item account-change",
|
|
4399
|
+
type: "button",
|
|
4400
|
+
role: "menuitem",
|
|
4401
|
+
onclick: () => void this.change()
|
|
4402
|
+
},
|
|
4403
|
+
this.t("account.change")
|
|
4404
|
+
)
|
|
4405
|
+
) : null;
|
|
4406
|
+
put(into, trigger, menu);
|
|
4407
|
+
if (menu) this.anchor(menu);
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Which side the menu hangs from.
|
|
4411
|
+
*
|
|
4412
|
+
* A header control is usually at the right edge, so the menu hangs right -
|
|
4413
|
+
* and the moment the header wraps on a narrow window, that same control sits
|
|
4414
|
+
* at the LEFT edge and the menu hangs off the screen. Measured rather than
|
|
4415
|
+
* assumed: it hangs left when there is room to the right, and right
|
|
4416
|
+
* otherwise, which lands correctly at both edges without the host declaring
|
|
4417
|
+
* where it put the control.
|
|
4418
|
+
*/
|
|
4419
|
+
anchor(menu) {
|
|
4420
|
+
const here = this.getBoundingClientRect();
|
|
4421
|
+
const fits = here.left + menu.getBoundingClientRect().width <= window.innerWidth - 8;
|
|
4422
|
+
menu.style.left = fits ? "0" : "auto";
|
|
4423
|
+
menu.style.right = fits ? "auto" : "0";
|
|
4424
|
+
}
|
|
4425
|
+
};
|
|
4426
|
+
|
|
3984
4427
|
// src/entries/widgets.ts
|
|
3985
4428
|
function defineSelfstoreWidgets(prefix = "selfstore") {
|
|
3986
4429
|
const define = (name, ctor) => {
|
|
@@ -3994,8 +4437,9 @@ function defineSelfstoreWidgets(prefix = "selfstore") {
|
|
|
3994
4437
|
define(`${prefix}-gate`, SelfstoreGateElement);
|
|
3995
4438
|
define(`${prefix}-destination`, SelfstoreDestinationElement);
|
|
3996
4439
|
define(`${prefix}-storage`, SelfstoreStorageElement);
|
|
4440
|
+
define(`${prefix}-account`, SelfstoreAccountElement);
|
|
3997
4441
|
}
|
|
3998
4442
|
|
|
3999
|
-
export { FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, SelfstoreStorageElement, defineSelfstoreWidgets };
|
|
4443
|
+
export { FlowWidget, SelfstoreAccountElement, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreDestinationElement, SelfstoreGateElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, SelfstoreStorageElement, defineSelfstoreWidgets };
|
|
4000
4444
|
//# sourceMappingURL=widgets.js.map
|
|
4001
4445
|
//# sourceMappingURL=widgets.js.map
|