selfstore 1.0.0

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/advanced.d.ts +90 -0
  4. package/dist/advanced.js +41 -0
  5. package/dist/advanced.js.map +1 -0
  6. package/dist/backups.d.ts +182 -0
  7. package/dist/backups.js +360 -0
  8. package/dist/backups.js.map +1 -0
  9. package/dist/cache-De5_sALA.d.ts +129 -0
  10. package/dist/cache-lock-LZILZNLP.js +23 -0
  11. package/dist/cache-lock-LZILZNLP.js.map +1 -0
  12. package/dist/chunk-ATUPHVLG.js +441 -0
  13. package/dist/chunk-ATUPHVLG.js.map +1 -0
  14. package/dist/chunk-BJ3RUMGI.js +45 -0
  15. package/dist/chunk-BJ3RUMGI.js.map +1 -0
  16. package/dist/chunk-CUWB5EWF.js +732 -0
  17. package/dist/chunk-CUWB5EWF.js.map +1 -0
  18. package/dist/chunk-FDPAKHQS.js +230 -0
  19. package/dist/chunk-FDPAKHQS.js.map +1 -0
  20. package/dist/chunk-IBIEKK36.js +331 -0
  21. package/dist/chunk-IBIEKK36.js.map +1 -0
  22. package/dist/chunk-JB4JHM7V.js +474 -0
  23. package/dist/chunk-JB4JHM7V.js.map +1 -0
  24. package/dist/chunk-N4RPMX3Y.js +1908 -0
  25. package/dist/chunk-N4RPMX3Y.js.map +1 -0
  26. package/dist/chunk-OWLX6PNT.js +231 -0
  27. package/dist/chunk-OWLX6PNT.js.map +1 -0
  28. package/dist/chunk-QBCU7OOP.js +20 -0
  29. package/dist/chunk-QBCU7OOP.js.map +1 -0
  30. package/dist/chunk-SGISYMT5.js +46 -0
  31. package/dist/chunk-SGISYMT5.js.map +1 -0
  32. package/dist/chunk-VBFMMR7Q.js +29 -0
  33. package/dist/chunk-VBFMMR7Q.js.map +1 -0
  34. package/dist/drive-Bv94UM8b.d.ts +222 -0
  35. package/dist/flows.d.ts +306 -0
  36. package/dist/flows.js +153 -0
  37. package/dist/flows.js.map +1 -0
  38. package/dist/groups.d.ts +39 -0
  39. package/dist/groups.js +103 -0
  40. package/dist/groups.js.map +1 -0
  41. package/dist/households.d.ts +214 -0
  42. package/dist/households.js +410 -0
  43. package/dist/households.js.map +1 -0
  44. package/dist/index.d.ts +205 -0
  45. package/dist/index.js +249 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kdf-worker.js +2395 -0
  48. package/dist/kdf-worker.js.map +1 -0
  49. package/dist/local-2py_vGGy.d.ts +47 -0
  50. package/dist/local-AM22RWRR.js +7 -0
  51. package/dist/local-AM22RWRR.js.map +1 -0
  52. package/dist/machine-ALMkT3mk.d.ts +17 -0
  53. package/dist/store-ChIGEOph.d.ts +577 -0
  54. package/dist/sync.d.ts +107 -0
  55. package/dist/sync.js +3 -0
  56. package/dist/sync.js.map +1 -0
  57. package/dist/widgets.d.ts +426 -0
  58. package/dist/widgets.js +2579 -0
  59. package/dist/widgets.js.map +1 -0
  60. package/llms.txt +405 -0
  61. package/package.json +99 -0
@@ -0,0 +1,2579 @@
1
+ import { connectFlow, shareFlow, joinFlow } from './chunk-ATUPHVLG.js';
2
+ import './chunk-CUWB5EWF.js';
3
+ import './chunk-JB4JHM7V.js';
4
+ import './chunk-SGISYMT5.js';
5
+ import './chunk-OWLX6PNT.js';
6
+ import './chunk-QBCU7OOP.js';
7
+ import './chunk-FDPAKHQS.js';
8
+ import './chunk-VBFMMR7Q.js';
9
+
10
+ // src/widgets/base.ts
11
+ var baseStyles = `
12
+ :host {
13
+ display: block;
14
+ font: inherit;
15
+ color: inherit;
16
+ --_accent: var(--selfstore-accent, #2563eb);
17
+ --_accent-ink: var(--selfstore-accent-contrast, #ffffff);
18
+ --_ink-dim: var(--selfstore-muted, color-mix(in srgb, currentColor 55%, transparent));
19
+ --_border: var(--selfstore-border, color-mix(in srgb, currentColor 16%, transparent));
20
+ --_radius: var(--selfstore-radius, 12px);
21
+ --_gap: var(--selfstore-gap, 0.6rem);
22
+ --_danger: var(--selfstore-danger, #dc2626);
23
+ --_ok: var(--selfstore-ok, #16a34a);
24
+ --_warn: var(--selfstore-warn, #d97706);
25
+ }
26
+ /* Status severity, as a text color the dot inherits (background: currentColor). */
27
+ [part~='sev-ok'] { color: var(--_ok); }
28
+ [part~='sev-info'] { color: var(--_accent); }
29
+ [part~='sev-warn'] { color: var(--_warn); }
30
+ [part~='sev-danger'] { color: var(--_danger); }
31
+ span[part~='status-dot'] {
32
+ display: inline-block;
33
+ width: 0.6em;
34
+ height: 0.6em;
35
+ border-radius: 50%;
36
+ background: currentColor;
37
+ flex-shrink: 0;
38
+ }
39
+ button[part~='dot-button'] {
40
+ background: none;
41
+ border: none;
42
+ padding: 0.2em;
43
+ cursor: pointer;
44
+ line-height: 0;
45
+ }
46
+ [part~='status-row'] [part~='status-action'] { margin-left: auto; }
47
+ [part~='card'] {
48
+ border: 1px solid var(--_border);
49
+ border-radius: var(--_radius);
50
+ padding: 0.8rem 0.9rem;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: var(--_gap);
54
+ width: 100%;
55
+ box-sizing: border-box;
56
+ background: transparent;
57
+ text-align: left;
58
+ }
59
+ button[part~='card'] { cursor: pointer; font: inherit; color: inherit; }
60
+ button[part~='card']:hover { border-color: var(--_accent); }
61
+ img[part~='icon'] {
62
+ width: var(--selfstore-icon-size, 2.1em);
63
+ height: var(--selfstore-icon-size, 2.1em);
64
+ object-fit: contain;
65
+ flex-shrink: 0;
66
+ }
67
+ [part~='stack'] { display: flex; flex-direction: column; gap: var(--_gap); container-type: inline-size; }
68
+ [part~='row'] { display: flex; align-items: center; gap: var(--_gap); flex-wrap: wrap; }
69
+ [part~='title'] { font-weight: 600; }
70
+ [part~='sub'], [part~='hint'] { color: var(--_ink-dim); font-size: 0.9em; }
71
+ [part~='tag'] {
72
+ display: inline-block;
73
+ font-size: 0.72em;
74
+ font-weight: 600;
75
+ line-height: 1.5;
76
+ padding: 0 0.55em;
77
+ margin-left: 0.5em;
78
+ border-radius: 999px;
79
+ background: var(--_accent);
80
+ color: var(--_accent-ink);
81
+ vertical-align: middle;
82
+ }
83
+ [part~='status'] {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: var(--_gap);
87
+ padding: 0.7rem 0.9rem;
88
+ border-radius: var(--_radius);
89
+ background: color-mix(in srgb, var(--_accent) 10%, transparent);
90
+ }
91
+ [part~='status-ok'] { background: color-mix(in srgb, var(--_ok) 12%, transparent); }
92
+ [part~='status-error'] { background: color-mix(in srgb, var(--_danger) 12%, transparent); }
93
+ [part='spinner'] {
94
+ width: 0.7em; height: 0.7em; flex-shrink: 0;
95
+ border-radius: 50%;
96
+ border: 2px solid var(--_accent);
97
+ border-top-color: transparent;
98
+ animation: ss-spin 0.8s linear infinite;
99
+ }
100
+ @keyframes ss-spin { to { transform: rotate(360deg); } }
101
+ button[part~='button'] {
102
+ font: inherit;
103
+ padding: 0.45rem 0.9rem;
104
+ border-radius: calc(var(--_radius) * 0.75);
105
+ border: 1px solid var(--_border);
106
+ background: transparent;
107
+ color: inherit;
108
+ cursor: pointer;
109
+ }
110
+ button[part~='button']:disabled { opacity: 0.55; cursor: default; }
111
+ button[part~='button-primary'] {
112
+ background: var(--_accent);
113
+ border-color: var(--_accent);
114
+ color: var(--_accent-ink);
115
+ }
116
+ button[part~='button-danger'] { color: var(--_danger); }
117
+ button[part~='link'] {
118
+ font: inherit;
119
+ background: none;
120
+ border: none;
121
+ padding: 0;
122
+ color: var(--_ink-dim);
123
+ text-decoration: underline;
124
+ cursor: pointer;
125
+ }
126
+ button[part~='link']:disabled { opacity: 0.55; cursor: default; }
127
+ button[part~='link-danger'] { color: var(--_danger); }
128
+ button[part~='advanced-link'] {
129
+ width: fit-content;
130
+ font-size: 0.85em;
131
+ margin-top: 0.1rem;
132
+ }
133
+ [part~='card'] [part~='row'] { margin-top: 0.45rem; }
134
+ input[part~='input'] {
135
+ font: inherit;
136
+ color: inherit;
137
+ background: transparent;
138
+ border: 1px solid var(--_border);
139
+ border-radius: calc(var(--_radius) * 0.75);
140
+ padding: 0.45rem 0.6rem;
141
+ width: 100%;
142
+ box-sizing: border-box;
143
+ }
144
+ input[part~='input']:focus { outline: 2px solid var(--_accent); outline-offset: 1px; }
145
+ /* A text input with a trailing button (the password eye) tucked inside it. */
146
+ [part~='field'] { position: relative; display: flex; align-items: center; width: 100%; }
147
+ [part~='field'] input[part~='input'] { padding-right: 2.4rem; }
148
+ button[part~='eye'] {
149
+ position: absolute;
150
+ right: 0.25rem;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ width: 2rem;
155
+ height: 2rem;
156
+ background: none;
157
+ border: none;
158
+ padding: 0;
159
+ cursor: pointer;
160
+ color: var(--_ink-dim);
161
+ }
162
+ button[part~='eye']:hover { color: inherit; }
163
+ button[part~='eye'] svg { width: 1.15em; height: 1.15em; display: block; }
164
+ [part~='warn-note'] { color: var(--_warn); }
165
+ button[part~='forgot-link'] { width: fit-content; font-size: 0.85em; margin-top: 0.15rem; }
166
+ [part~='error-note'] { color: var(--_danger); font-size: 0.9em; }
167
+ /* WebDAV/S3 segmented control: a rail with the inactive segments quiet and the
168
+ active one lifted onto the surface. Full-width segments, equal share. */
169
+ [part~='tabs'] {
170
+ display: flex;
171
+ gap: 0.2rem;
172
+ padding: 0.25rem;
173
+ border-radius: calc(var(--_radius) * 0.7);
174
+ background: color-mix(in srgb, currentColor 8%, transparent);
175
+ }
176
+ button[part~='tab'] {
177
+ flex: 1 1 0;
178
+ font: inherit;
179
+ font-weight: 600;
180
+ padding: 0.4rem 0.7rem;
181
+ border: none;
182
+ border-radius: calc(var(--_radius) * 0.5);
183
+ background: transparent;
184
+ color: var(--_ink-dim);
185
+ cursor: pointer;
186
+ }
187
+ button[part~='tab']:hover { color: inherit; }
188
+ button[part~='tab-on'] {
189
+ background: Canvas;
190
+ color: inherit;
191
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
192
+ }
193
+ /* WebDAV provider quick-pick chips. */
194
+ [part~='presets'] { display: flex; flex-wrap: wrap; gap: 0.4rem; }
195
+ button[part~='preset'] {
196
+ font: inherit;
197
+ font-size: 0.85em;
198
+ padding: 0.3rem 0.75rem;
199
+ border: 1px solid var(--_border);
200
+ border-radius: 999px;
201
+ background: transparent;
202
+ color: inherit;
203
+ cursor: pointer;
204
+ }
205
+ button[part~='preset']:hover { border-color: var(--_accent); }
206
+ button[part~='preset-on'] {
207
+ border-color: var(--_accent);
208
+ background: color-mix(in srgb, var(--_accent) 12%, transparent);
209
+ }
210
+ ul[part~='list'] { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--_gap); }
211
+ img[part~='qr'] {
212
+ width: var(--selfstore-qr-size, 6.5em);
213
+ height: var(--selfstore-qr-size, 6.5em);
214
+ border-radius: calc(var(--_radius) * 0.5);
215
+ background: #ffffff;
216
+ flex-shrink: 0;
217
+ }
218
+ /* Overflow menu. The trigger is a quiet in-card button; the open menu is a
219
+ dropdown over the trigger's corner on wide screens and a bottom sheet on
220
+ phones. The backdrop catches every outside click or tap (the standard
221
+ dismiss), sitting above the sibling cards but under the panel. */
222
+ button[part~='menu-button'] {
223
+ border: none;
224
+ background: none;
225
+ padding: 0.25rem 0.6rem;
226
+ font-size: 1.1em;
227
+ line-height: 1.3;
228
+ border-radius: calc(var(--_radius) * 0.5);
229
+ color: var(--_ink-dim);
230
+ cursor: pointer;
231
+ }
232
+ button[part~='menu-button']:hover,
233
+ button[part~='menu-button'][aria-expanded='true'] {
234
+ background: color-mix(in srgb, currentColor 10%, transparent);
235
+ color: inherit;
236
+ }
237
+ [part~='card'] > button[part~='menu-button'] { align-self: flex-start; }
238
+ [part~='menu-layer'] { display: contents; }
239
+ [part~='menu-backdrop'] { position: fixed; inset: 0; z-index: 29; background: transparent; }
240
+ [part~='menu'] {
241
+ position: absolute;
242
+ right: 0.4rem;
243
+ top: 0.4rem;
244
+ z-index: 30;
245
+ display: flex;
246
+ flex-direction: column;
247
+ min-width: 11rem;
248
+ padding: 0.3rem 0;
249
+ border: 1px solid var(--_border);
250
+ border-radius: calc(var(--_radius) * 0.75);
251
+ background: Canvas;
252
+ color: CanvasText;
253
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
254
+ outline: none;
255
+ }
256
+ button[part~='menu-item'] {
257
+ font: inherit;
258
+ text-align: left;
259
+ padding: 0.55rem 0.95rem;
260
+ border: none;
261
+ background: none;
262
+ color: inherit;
263
+ cursor: pointer;
264
+ white-space: nowrap;
265
+ }
266
+ button[part~='menu-item']:hover,
267
+ button[part~='menu-item']:focus-visible {
268
+ background: color-mix(in srgb, var(--_accent) 10%, transparent);
269
+ }
270
+ button[part~='menu-item-danger'] { color: var(--_danger); }
271
+ @media (pointer: coarse) {
272
+ button[part~='menu-item'] { padding: 0.8rem 1rem; }
273
+ }
274
+ @media (max-width: 500px) {
275
+ [part~='menu-backdrop'] { background: rgba(0, 0, 0, 0.35); }
276
+ [part~='menu'] {
277
+ position: fixed;
278
+ inset: auto 0 0 0;
279
+ border-radius: var(--_radius) var(--_radius) 0 0;
280
+ border-bottom: none;
281
+ min-width: 0;
282
+ padding: 0.4rem 0 max(0.5rem, env(safe-area-inset-bottom));
283
+ box-shadow: 0 -12px 34px rgba(0, 0, 0, 0.28);
284
+ }
285
+ button[part~='menu-item'] { padding: 0.95rem 1.2rem; }
286
+ }
287
+ /* Responsive by default. The stack div above is the query container, so a
288
+ widget reacts to ITS OWN width (a narrow column on a wide screen counts),
289
+ and the host element's own sizing stays untouched. When the widget is
290
+ narrow, a list card's row (QR + url + copy + revoke) would squeeze the
291
+ text into a sliver: stack the card and centre the QR instead. */
292
+ @container (max-width: 480px) {
293
+ li[part~='card'] {
294
+ flex-direction: column;
295
+ align-items: stretch;
296
+ }
297
+ li[part~='card'] img[part~='qr'] { align-self: center; }
298
+ /* Stacked cards must not push the overflow trigger below the body: it
299
+ stays a top-right kebab (the card is position:relative already). */
300
+ li[part~='card']:has(> button[part~='menu-button']) { padding-right: 2.4rem; }
301
+ li[part~='card'] > button[part~='menu-button'] {
302
+ position: absolute;
303
+ top: 0.4rem;
304
+ right: 0.4rem;
305
+ }
306
+ }
307
+ `;
308
+ function put(el, ...children) {
309
+ for (const c of children) {
310
+ if (c == null) continue;
311
+ el.append(typeof c === "string" ? document.createTextNode(c) : c);
312
+ }
313
+ }
314
+ function h(tag, attrs = {}, ...children) {
315
+ const el = document.createElement(tag);
316
+ for (const [k, v] of Object.entries(attrs)) {
317
+ if (v == null || v === false) continue;
318
+ if (k.startsWith("on") && typeof v === "function") {
319
+ el.addEventListener(k.slice(2), v);
320
+ } else if (v === true) {
321
+ el.setAttribute(k, "");
322
+ } else {
323
+ el.setAttribute(k, String(v));
324
+ }
325
+ }
326
+ for (const c of children) {
327
+ if (c == null) continue;
328
+ el.append(typeof c === "string" ? document.createTextNode(c) : c);
329
+ }
330
+ return el;
331
+ }
332
+ var FlowWidget = class extends HTMLElement {
333
+ root;
334
+ styleEl;
335
+ body;
336
+ overrides = {};
337
+ unsub = null;
338
+ constructor() {
339
+ super();
340
+ this.root = this.attachShadow({ mode: "open" });
341
+ this.styleEl = document.createElement("style");
342
+ this.styleEl.textContent = baseStyles;
343
+ this.body = document.createElement("div");
344
+ this.body.setAttribute("part", "stack");
345
+ this.root.append(this.styleEl, this.body);
346
+ }
347
+ /** Partial override map, merged over the EN defaults (also the i18n hook). */
348
+ get labels() {
349
+ return this.overrides;
350
+ }
351
+ set labels(map) {
352
+ this.overrides = map ?? {};
353
+ this.rerender();
354
+ }
355
+ t(key) {
356
+ return this.overrides[key] ?? this.defaults()[key] ?? key;
357
+ }
358
+ /** A heading an empty-string label removes: labels = { 'share.title': '' }
359
+ * hides that heading (the host page already says it) without a fork.
360
+ * Append through put(), which skips the null. */
361
+ heading(part, key) {
362
+ const text = this.t(key);
363
+ return text ? h("div", { part }, text) : null;
364
+ }
365
+ /** Map a flow error to copy: labels can override per labelKey
366
+ * ('error.targetUnavailable', ...), with a generic fallback. */
367
+ errorText(labelKey) {
368
+ if (labelKey) {
369
+ const specific = this.overrides[labelKey] ?? this.defaults()[labelKey];
370
+ if (specific) return specific;
371
+ }
372
+ return this.t("error.generic");
373
+ }
374
+ emit(name, detail) {
375
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
376
+ }
377
+ /** Rebuild the body, keeping the focused input's value and focus across the
378
+ * rebuild (inputs carry data-keep so typing survives a snapshot change). */
379
+ rerender() {
380
+ let active = null;
381
+ try {
382
+ active = this.root.activeElement;
383
+ } catch {
384
+ active = null;
385
+ }
386
+ const keep = active?.getAttribute?.("data-keep") ?? null;
387
+ const value = active?.value;
388
+ this.body.replaceChildren();
389
+ this.view(this.body);
390
+ if (keep != null) {
391
+ const again = this.body.querySelector(`[data-keep="${keep}"]`);
392
+ if (again) {
393
+ if (value != null) again.value = value;
394
+ again.focus();
395
+ }
396
+ }
397
+ }
398
+ disconnectedCallback() {
399
+ this.unsub?.();
400
+ this.unsub = null;
401
+ }
402
+ };
403
+
404
+ // src/widgets/connect.ts
405
+ var EN = {
406
+ "connect.title": "Where should we save your data?",
407
+ "connect.recommended": "Recommended",
408
+ "connect.drive": "Google Drive",
409
+ "connect.drive.sub": "Available on all your devices",
410
+ "connect.file": "A file on this device",
411
+ "connect.file.sub": "Offline, you keep the file",
412
+ "connect.file.new": "New backup",
413
+ "connect.file.open": "Load an existing file",
414
+ "connect.webdav": "My own server (WebDAV)",
415
+ "connect.webdav.sub": "Nextcloud, ownCloud, yours",
416
+ "connect.s3": "My own bucket (S3)",
417
+ "connect.s3.sub": "Amazon S3, R2, B2, MinIO",
418
+ "connect.server": "My own server",
419
+ "connect.server.sub": "WebDAV or an S3 bucket you control",
420
+ "connect.tab.webdav": "WebDAV",
421
+ "connect.tab.s3": "S3",
422
+ "connect.s3.endpoint": "Endpoint URL",
423
+ "connect.s3.region": "Region",
424
+ "connect.s3.bucket": "Bucket",
425
+ "connect.s3.key": "Object key (path)",
426
+ "connect.s3.accessKeyId": "Access key ID",
427
+ "connect.s3.secret": "Secret access key",
428
+ "connect.s3.submit": "Connect",
429
+ "connect.connecting": "Connecting...",
430
+ "connect.connecting.drive": "Authorise access in the Google window.",
431
+ "connect.cancel": "Cancel",
432
+ "connect.retry": "Try again",
433
+ "connect.password.title": "This backup is protected",
434
+ "connect.password.hint": "Enter its password to open it.",
435
+ "connect.password.placeholder": "Password",
436
+ "connect.password.wrong": "Wrong password. Try again.",
437
+ "connect.password.submit": "Open",
438
+ "connect.password.show": "Show password",
439
+ "connect.password.hide": "Hide password",
440
+ "connect.password.forgot": "Forgot the password?",
441
+ "connect.password.forgot.warn": "Without the password, this backup cannot be opened. You can erase it and start from an empty one. The old backup is lost for good.",
442
+ "connect.password.forgot.confirm": "Overwrite the backup",
443
+ "connect.password.forgot.back": "Back",
444
+ "connect.conflict.title": "This destination already holds a backup",
445
+ "connect.conflict.merge": "Merge both",
446
+ "connect.conflict.merge.sub": "Keep everything from both sides",
447
+ "connect.conflict.resume": "Use the backup",
448
+ "connect.conflict.resume.sub": "This device adopts it",
449
+ "connect.conflict.replace": "Replace the backup",
450
+ "connect.conflict.replace.sub": "This device wins, the backup is overwritten",
451
+ "connect.webdav.url": "Server URL",
452
+ "connect.webdav.user": "Username",
453
+ "connect.webdav.password": "Password",
454
+ "connect.webdav.submit": "Connect",
455
+ "connect.webdav.signup": "Create an account",
456
+ "connect.webdav.help.more": "How?",
457
+ "connect.done": "Connected. Your data is saved.",
458
+ "connect.done.manual": "Download mode: save the file after each change.",
459
+ "error.generic": "That did not work. Check the connection and try again.",
460
+ "error.targetUnavailable": "The destination did not answer. Try again in a moment.",
461
+ "error.authExpired": "Access expired: reconnect to continue.",
462
+ "error.decryptFailed": "This backup could not be opened with that password.",
463
+ "error.badFormat": "This file does not look like a readable backup."
464
+ };
465
+ var EYE_SHOW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3-8 10-8 10 8 10 8-3 8-10 8-10-8-10-8Z"/><circle cx="12" cy="12" r="3"/></svg>';
466
+ var EYE_HIDE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 10 8 10 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><path d="M6.61 6.61A18.45 18.45 0 0 0 2 12s3 8 10 8a9.12 9.12 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></svg>';
467
+ function eyeIcon(visible) {
468
+ const span = document.createElement("span");
469
+ span.innerHTML = visible ? EYE_HIDE : EYE_SHOW;
470
+ return span.firstElementChild;
471
+ }
472
+ var SelfstoreConnectElement = class extends FlowWidget {
473
+ static get observedAttributes() {
474
+ return ["recommended", "advanced"];
475
+ }
476
+ #store = null;
477
+ #targets = null;
478
+ #options = {};
479
+ #flow = null;
480
+ #recommended = null;
481
+ #icons = {};
482
+ #advanced = [];
483
+ #webdavPresets = [];
484
+ #webdavUrl = "";
485
+ #webdavUser = "";
486
+ #webdavPass = "";
487
+ #pickedPreset = null;
488
+ // S3 form fields (kept on the element, like the WebDAV ones, so switching the
489
+ // server tab never loses what was typed).
490
+ #s3Endpoint = "";
491
+ #s3Region = "";
492
+ #s3Bucket = "";
493
+ #s3Key = "";
494
+ #s3AccessKeyId = "";
495
+ #s3Secret = "";
496
+ // Password-step local UI state (never in the flow: purely how this element
497
+ // draws the step). Reset whenever the flow leaves 'password' (see wire()).
498
+ #pwVisible = false;
499
+ #confirmOverwrite = false;
500
+ defaults() {
501
+ return EN;
502
+ }
503
+ /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
504
+ get store() {
505
+ return this.#store;
506
+ }
507
+ set store(v) {
508
+ this.#store = v;
509
+ this.wire();
510
+ }
511
+ get targets() {
512
+ return this.#targets;
513
+ }
514
+ set targets(v) {
515
+ this.#targets = v;
516
+ this.wire();
517
+ }
518
+ get options() {
519
+ return this.#options;
520
+ }
521
+ set options(v) {
522
+ this.#options = v ?? {};
523
+ this.wire();
524
+ }
525
+ /** Badge one destination as the one to pick (part="tag", label
526
+ * 'connect.recommended'). Purely a highlight: the order still follows the
527
+ * targets object. Also the `recommended` attribute. Default: none. */
528
+ get recommended() {
529
+ return this.#recommended;
530
+ }
531
+ set recommended(v) {
532
+ this.#recommended = v === "drive" || v === "file" || v === "webdav" || v === "s3" ? v : null;
533
+ this.rerender();
534
+ }
535
+ /** An optional icon per destination - an image URL or data URI, rendered as
536
+ * part="icon" at the head of that card. Purely decorative and per-instance;
537
+ * omit a kind for no icon. Size it with --selfstore-icon-size. */
538
+ get icons() {
539
+ return this.#icons;
540
+ }
541
+ set icons(v) {
542
+ this.#icons = v ?? {};
543
+ this.rerender();
544
+ }
545
+ /** Destinations to tuck behind a discreet link under the cards instead of a
546
+ * full card - for power-user options (say, WebDAV) that must stay reachable
547
+ * without weighing the everyday choice. Also the `advanced` attribute
548
+ * (comma-separated). The journey after the click is exactly the same. */
549
+ get advanced() {
550
+ return this.#advanced;
551
+ }
552
+ set advanced(v) {
553
+ this.#advanced = (v ?? []).filter(
554
+ (k) => k === "drive" || k === "file" || k === "webdav" || k === "s3"
555
+ );
556
+ this.rerender();
557
+ }
558
+ /** Named WebDAV providers offered as a quick-pick row above the WebDAV form,
559
+ * pre-filling the URL field. The app owns the list (its own host, sovereign
560
+ * providers, ...); selfstore ships none. Purely additive - the blank form
561
+ * still works. */
562
+ get webdavPresets() {
563
+ return this.#webdavPresets;
564
+ }
565
+ set webdavPresets(v) {
566
+ this.#webdavPresets = Array.isArray(v) ? v : [];
567
+ this.rerender();
568
+ }
569
+ /** The underlying flow, for programmatic control (may be null until wired). */
570
+ get flow() {
571
+ return this.#flow;
572
+ }
573
+ attributeChangedCallback(name, _old, value) {
574
+ if (name === "recommended") this.recommended = value;
575
+ if (name === "advanced") {
576
+ this.advanced = (value ?? "").split(",").map((s) => s.trim());
577
+ }
578
+ }
579
+ connectedCallback() {
580
+ if (this.#recommended === null && this.hasAttribute("recommended")) {
581
+ this.recommended = this.getAttribute("recommended");
582
+ }
583
+ if (this.#advanced.length === 0 && this.hasAttribute("advanced")) {
584
+ this.advanced = (this.getAttribute("advanced") ?? "").split(",").map((s) => s.trim());
585
+ }
586
+ this.wire();
587
+ }
588
+ wire() {
589
+ if (!this.isConnected || !this.#store || !this.#targets) return;
590
+ this.unsub?.();
591
+ this.#flow = connectFlow(this.#store, this.#targets, this.#options);
592
+ let last = null;
593
+ this.unsub = this.#flow.subscribe((snap) => {
594
+ const was = last;
595
+ last = snap;
596
+ if (snap.step === "connected" && was?.step !== "connected") {
597
+ this.emit("selfstore-connected", { outcome: snap.outcome });
598
+ }
599
+ if (snap.step === "error" && was?.step !== "error") {
600
+ this.emit("selfstore-error", { error: snap.error });
601
+ }
602
+ if (snap.step !== "password") {
603
+ this.#pwVisible = false;
604
+ this.#confirmOverwrite = false;
605
+ }
606
+ this.rerender();
607
+ });
608
+ }
609
+ view(into) {
610
+ const flow = this.#flow;
611
+ if (!flow) return;
612
+ const s = flow.snapshot;
613
+ if (s.step === "choose") {
614
+ put(into, this.heading("title", "connect.title"));
615
+ const serverKinds = ["webdav", "s3"].filter(
616
+ (k) => s.kinds.includes(k) && !this.#advanced.includes(k)
617
+ );
618
+ const groupServer = serverKinds.length >= 2;
619
+ for (const kind of s.kinds) {
620
+ if (this.#advanced.includes(kind)) continue;
621
+ if (groupServer && (kind === "webdav" || kind === "s3")) {
622
+ if (kind === serverKinds[0]) into.append(this.serverCard(serverKinds[0]));
623
+ continue;
624
+ }
625
+ into.append(this.destCard(kind));
626
+ }
627
+ for (const kind of s.kinds) {
628
+ if (this.#advanced.includes(kind)) {
629
+ into.append(
630
+ h(
631
+ "button",
632
+ { part: "link advanced-link", "data-kind": kind, onclick: () => this.#flow?.choose(kind) },
633
+ this.t(`connect.${kind}`)
634
+ )
635
+ );
636
+ }
637
+ }
638
+ return;
639
+ }
640
+ if (s.step === "form") {
641
+ const kind = s.kind === "s3" ? "s3" : "webdav";
642
+ const tabbed = ["webdav", "s3"].filter((k) => this.#targets?.[k] === true).length >= 2;
643
+ const children = [
644
+ h("div", { part: "title" }, this.t(tabbed ? "connect.server" : `connect.${kind}`))
645
+ ];
646
+ if (tabbed) children.push(this.serverTabs(kind));
647
+ children.push(
648
+ ...kind === "s3" ? this.s3FormChildren(flow) : this.webdavFormChildren(flow)
649
+ );
650
+ into.append(...children);
651
+ return;
652
+ }
653
+ if (s.step === "authorizing") {
654
+ into.append(
655
+ h(
656
+ "div",
657
+ { part: "status" },
658
+ h("span", { part: "spinner", "aria-hidden": "true" }),
659
+ h(
660
+ "div",
661
+ {},
662
+ h("div", { part: "title" }, this.t("connect.connecting")),
663
+ s.kind === "drive" ? h("div", { part: "sub" }, this.t("connect.connecting.drive")) : null
664
+ ),
665
+ h("button", { part: "button", onclick: () => this.cancel() }, this.t("connect.cancel"))
666
+ )
667
+ );
668
+ return;
669
+ }
670
+ if (s.step === "password") {
671
+ if (this.#confirmOverwrite) {
672
+ put(
673
+ into,
674
+ h("div", { part: "title" }, this.t("connect.password.title")),
675
+ h("div", { part: "hint warn-note" }, this.t("connect.password.forgot.warn")),
676
+ h(
677
+ "div",
678
+ { part: "row" },
679
+ h(
680
+ "button",
681
+ { part: "button button-danger", disabled: s.busy, onclick: () => this.doOverwrite() },
682
+ this.t("connect.password.forgot.confirm")
683
+ ),
684
+ h(
685
+ "button",
686
+ { part: "button", disabled: s.busy, onclick: () => this.setConfirmOverwrite(false) },
687
+ this.t("connect.password.forgot.back")
688
+ )
689
+ )
690
+ );
691
+ return;
692
+ }
693
+ const field = h(
694
+ "div",
695
+ { part: "field" },
696
+ h("input", {
697
+ part: "input",
698
+ type: this.#pwVisible ? "text" : "password",
699
+ placeholder: this.t("connect.password.placeholder"),
700
+ "data-keep": "password",
701
+ autocomplete: "current-password"
702
+ }),
703
+ h(
704
+ "button",
705
+ {
706
+ part: "eye",
707
+ type: "button",
708
+ "aria-label": this.t(this.#pwVisible ? "connect.password.hide" : "connect.password.show"),
709
+ "aria-pressed": this.#pwVisible ? "true" : "false",
710
+ onclick: () => this.togglePassword()
711
+ },
712
+ eyeIcon(this.#pwVisible)
713
+ )
714
+ );
715
+ put(
716
+ into,
717
+ h("div", { part: "title" }, this.t("connect.password.title")),
718
+ h("div", { part: "hint" }, this.t("connect.password.hint")),
719
+ field,
720
+ s.passwordError ? h("div", { part: "error-note" }, this.t("connect.password.wrong")) : null,
721
+ h(
722
+ "div",
723
+ { part: "row" },
724
+ h(
725
+ "button",
726
+ { part: "button button-primary", disabled: s.busy, onclick: () => this.submitPassword() },
727
+ this.t("connect.password.submit")
728
+ ),
729
+ // Cancel is never disabled, even mid-trial-read: a hung leg must
730
+ // always have a working way out. The generation counter makes a
731
+ // late resolution of the abandoned leg a no-op, so leaving is safe.
732
+ h("button", { part: "button", onclick: () => this.cancel() }, this.t("connect.cancel"))
733
+ ),
734
+ // The forgotten-password escape: a discreet link into the confirm
735
+ // sub-state above (erasing the backup is never one careless click).
736
+ h(
737
+ "button",
738
+ { part: "link forgot-link", onclick: () => this.setConfirmOverwrite(true) },
739
+ this.t("connect.password.forgot")
740
+ )
741
+ );
742
+ return;
743
+ }
744
+ if (s.step === "conflict") {
745
+ into.append(h("div", { part: "title" }, this.t("connect.conflict.title")));
746
+ for (const how of ["merge", "resume", "replace"]) {
747
+ into.append(
748
+ h(
749
+ "button",
750
+ { part: "card", disabled: s.busy, onclick: () => flow.resolveConflict(how) },
751
+ h(
752
+ "div",
753
+ {},
754
+ h("div", { part: "title" }, this.t(`connect.conflict.${how}`)),
755
+ h("div", { part: "sub" }, this.t(`connect.conflict.${how}.sub`))
756
+ )
757
+ )
758
+ );
759
+ }
760
+ into.append(h("button", { part: "link", onclick: () => this.cancel() }, this.t("connect.cancel")));
761
+ return;
762
+ }
763
+ if (s.step === "connected") {
764
+ into.append(
765
+ h(
766
+ "div",
767
+ { part: "status status-ok" },
768
+ h("div", { part: "title" }, this.t(s.outcome === "manual" ? "connect.done.manual" : "connect.done"))
769
+ )
770
+ );
771
+ return;
772
+ }
773
+ into.append(
774
+ h("div", { part: "status status-error" }, h("div", { part: "title" }, this.errorText(s.error?.labelKey))),
775
+ h(
776
+ "div",
777
+ { part: "row" },
778
+ h("button", { part: "button button-primary", onclick: () => flow.retry() }, this.t("connect.retry")),
779
+ h("button", { part: "button", onclick: () => this.cancel() }, this.t("connect.cancel"))
780
+ )
781
+ );
782
+ }
783
+ destCard(kind) {
784
+ const icon = this.#icons[kind];
785
+ const head = icon ? h("img", { part: "icon", src: icon, alt: "" }) : null;
786
+ const title = h(
787
+ "div",
788
+ { part: "title" },
789
+ this.t(`connect.${kind}`),
790
+ this.#recommended === kind ? h("span", { part: "tag" }, this.t("connect.recommended")) : null
791
+ );
792
+ const sub = h("div", { part: "sub" }, this.t(`connect.${kind}.sub`));
793
+ const fileSpec = kind === "file" ? this.#targets?.file : null;
794
+ if (fileSpec && typeof fileSpec === "object") {
795
+ const openSupported = typeof window !== "undefined" && "showOpenFilePicker" in window;
796
+ const actions = h("div", { part: "row" });
797
+ if (fileSpec.create != null) {
798
+ actions.append(
799
+ h(
800
+ "button",
801
+ { part: "button button-primary", "data-action": "create", onclick: () => this.#flow?.choose("file", "create") },
802
+ this.t("connect.file.new")
803
+ )
804
+ );
805
+ }
806
+ if (fileSpec.open != null && (typeof fileSpec.open === "function" || openSupported)) {
807
+ actions.append(
808
+ h(
809
+ "button",
810
+ { part: "button", "data-action": "open", onclick: () => this.#flow?.choose("file", "open") },
811
+ this.t("connect.file.open")
812
+ )
813
+ );
814
+ }
815
+ const body = h("div", {}, title, sub);
816
+ if (actions.childElementCount > 0) body.append(actions);
817
+ return h("div", { part: "card", "data-kind": kind }, head, body);
818
+ }
819
+ return h(
820
+ "button",
821
+ { part: "card", "data-kind": kind, onclick: () => this.#flow?.choose(kind) },
822
+ head,
823
+ h("div", {}, title, sub)
824
+ );
825
+ }
826
+ /** The grouped "your own server" card shown when both WebDAV and S3 are
827
+ * offered: one entry into the tabbed form, opening on the first server kind. */
828
+ serverCard(defaultKind) {
829
+ const recommended = this.#recommended === "webdav" || this.#recommended === "s3";
830
+ return h(
831
+ "button",
832
+ { part: "card", "data-kind": "server", onclick: () => this.#flow?.choose(defaultKind) },
833
+ h(
834
+ "div",
835
+ {},
836
+ h(
837
+ "div",
838
+ { part: "title" },
839
+ this.t("connect.server"),
840
+ recommended ? h("span", { part: "tag" }, this.t("connect.recommended")) : null
841
+ ),
842
+ h("div", { part: "sub" }, this.t("connect.server.sub"))
843
+ )
844
+ );
845
+ }
846
+ /** WebDAV/S3 segmented toggle. Switching leaves the form and re-enters it on
847
+ * the other kind - no flow surgery - and the typed fields live on the element,
848
+ * so nothing the user wrote is lost across the switch. */
849
+ serverTabs(active) {
850
+ const tab = (k) => h(
851
+ "button",
852
+ {
853
+ part: active === k ? "tab tab-on" : "tab",
854
+ type: "button",
855
+ "data-tab": k,
856
+ "aria-pressed": active === k ? "true" : "false",
857
+ onclick: () => {
858
+ if (active === k) return;
859
+ this.#flow?.cancel();
860
+ this.#flow?.choose(k);
861
+ }
862
+ },
863
+ this.t(`connect.tab.${k}`)
864
+ );
865
+ return h("div", { part: "tabs" }, tab("webdav"), tab("s3"));
866
+ }
867
+ webdavFormChildren(flow) {
868
+ const picked = this.#pickedPreset;
869
+ const out = [];
870
+ if (this.#webdavPresets.length > 0) {
871
+ out.push(
872
+ h(
873
+ "div",
874
+ { part: "presets" },
875
+ ...this.#webdavPresets.map(
876
+ (p) => h(
877
+ "button",
878
+ {
879
+ part: picked?.id === p.id ? "preset preset-on" : "preset",
880
+ type: "button",
881
+ "data-preset": p.id,
882
+ "aria-pressed": picked?.id === p.id ? "true" : "false",
883
+ title: p.help ?? "",
884
+ onclick: () => this.pickWebdavPreset(p)
885
+ },
886
+ p.label
887
+ )
888
+ )
889
+ )
890
+ );
891
+ }
892
+ out.push(
893
+ h("input", {
894
+ part: "input",
895
+ type: "url",
896
+ placeholder: this.t("connect.webdav.url"),
897
+ "data-keep": "wd-url",
898
+ value: this.#webdavUrl,
899
+ oninput: (e) => this.#webdavUrl = e.target.value
900
+ }),
901
+ h("input", {
902
+ part: "input",
903
+ type: "text",
904
+ placeholder: this.t("connect.webdav.user"),
905
+ "data-keep": "wd-user",
906
+ value: this.#webdavUser,
907
+ oninput: (e) => this.#webdavUser = e.target.value
908
+ }),
909
+ h("input", {
910
+ part: "input",
911
+ type: "password",
912
+ placeholder: this.t("connect.webdav.password"),
913
+ "data-keep": "wd-pass",
914
+ value: this.#webdavPass,
915
+ oninput: (e) => this.#webdavPass = e.target.value
916
+ })
917
+ );
918
+ if (picked?.help) {
919
+ out.push(
920
+ h(
921
+ "div",
922
+ { part: "hint webdav-note" },
923
+ picked.help,
924
+ picked.helpUrl ? h(
925
+ "a",
926
+ { part: "link webdav-help", href: picked.helpUrl, target: "_blank", rel: "noopener noreferrer" },
927
+ ` ${this.t("connect.webdav.help.more")}`
928
+ ) : null
929
+ )
930
+ );
931
+ }
932
+ out.push(
933
+ h(
934
+ "div",
935
+ { part: "row" },
936
+ h("button", { part: "button button-primary", onclick: () => this.submitWebdav() }, this.t("connect.webdav.submit")),
937
+ h("button", { part: "button", onclick: () => flow.cancel() }, this.t("connect.cancel"))
938
+ )
939
+ );
940
+ if (picked?.signupUrl) {
941
+ out.push(
942
+ h(
943
+ "a",
944
+ { part: "link webdav-signup", href: picked.signupUrl, target: "_blank", rel: "noopener noreferrer" },
945
+ this.t("connect.webdav.signup")
946
+ )
947
+ );
948
+ }
949
+ return out;
950
+ }
951
+ s3FormChildren(flow) {
952
+ const field = (type, label, keep, get, set) => h("input", {
953
+ part: "input",
954
+ type,
955
+ placeholder: this.t(label),
956
+ "data-keep": keep,
957
+ value: get(),
958
+ oninput: (e) => set(e.target.value)
959
+ });
960
+ return [
961
+ field("url", "connect.s3.endpoint", "s3-endpoint", () => this.#s3Endpoint, (v) => this.#s3Endpoint = v),
962
+ field("text", "connect.s3.region", "s3-region", () => this.#s3Region, (v) => this.#s3Region = v),
963
+ field("text", "connect.s3.bucket", "s3-bucket", () => this.#s3Bucket, (v) => this.#s3Bucket = v),
964
+ field("text", "connect.s3.key", "s3-key", () => this.#s3Key, (v) => this.#s3Key = v),
965
+ field("text", "connect.s3.accessKeyId", "s3-akid", () => this.#s3AccessKeyId, (v) => this.#s3AccessKeyId = v),
966
+ field("password", "connect.s3.secret", "s3-secret", () => this.#s3Secret, (v) => this.#s3Secret = v),
967
+ h(
968
+ "div",
969
+ { part: "row" },
970
+ h("button", { part: "button button-primary", onclick: () => this.submitS3() }, this.t("connect.s3.submit")),
971
+ h("button", { part: "button", onclick: () => flow.cancel() }, this.t("connect.cancel"))
972
+ )
973
+ ];
974
+ }
975
+ submitS3() {
976
+ const endpoint = this.#s3Endpoint.trim();
977
+ const region = this.#s3Region.trim();
978
+ const bucket = this.#s3Bucket.trim();
979
+ const key = this.#s3Key.trim();
980
+ if (!endpoint || !region || !bucket || !key) return;
981
+ const config = {
982
+ endpoint,
983
+ region,
984
+ bucket,
985
+ key,
986
+ accessKeyId: this.#s3AccessKeyId.trim(),
987
+ secretAccessKey: this.#s3Secret
988
+ };
989
+ this.#flow?.submitS3(config);
990
+ }
991
+ cancel() {
992
+ this.#flow?.cancel();
993
+ this.emit("selfstore-cancelled");
994
+ }
995
+ submitPassword() {
996
+ const input = this.root.querySelector('[data-keep="password"]');
997
+ if (input) this.#flow?.submitPassword(input.value);
998
+ }
999
+ /** Flip the password field between hidden and shown IN PLACE - swap the
1000
+ * input type and the eye glyph without a re-render, so what the user has
1001
+ * already typed survives the toggle. */
1002
+ togglePassword() {
1003
+ this.#pwVisible = !this.#pwVisible;
1004
+ const input = this.root.querySelector('[data-keep="password"]');
1005
+ if (input) {
1006
+ input.type = this.#pwVisible ? "text" : "password";
1007
+ input.focus();
1008
+ }
1009
+ const eye = this.root.querySelector('[part~="eye"]');
1010
+ if (eye) {
1011
+ eye.setAttribute(
1012
+ "aria-label",
1013
+ this.t(this.#pwVisible ? "connect.password.hide" : "connect.password.show")
1014
+ );
1015
+ eye.setAttribute("aria-pressed", this.#pwVisible ? "true" : "false");
1016
+ eye.replaceChildren(eyeIcon(this.#pwVisible));
1017
+ }
1018
+ }
1019
+ /** Enter or leave the forgotten-password confirmation sub-state. */
1020
+ setConfirmOverwrite(on) {
1021
+ this.#confirmOverwrite = on;
1022
+ this.rerender();
1023
+ }
1024
+ /** Confirmed: abandon the forgotten password and start a fresh backup over
1025
+ * the protected one (the flow makes this device win). */
1026
+ doOverwrite() {
1027
+ this.#flow?.overwrite();
1028
+ }
1029
+ /** Fill the WebDAV URL field from a picked provider preset and move focus on to
1030
+ * the username, so the pick just seeds the form the user then completes. The
1031
+ * pick is remembered so its guidance / sign-up link render and its chip flags
1032
+ * as active; a re-render keeps the already-typed username and password. */
1033
+ pickWebdavPreset(p) {
1034
+ this.#pickedPreset = p;
1035
+ this.#webdavUrl = p.url ?? "";
1036
+ this.rerender();
1037
+ this.root.querySelector('[data-keep="wd-user"]')?.focus();
1038
+ }
1039
+ submitWebdav() {
1040
+ const url = this.#webdavUrl.trim();
1041
+ if (!url) return;
1042
+ const config = {
1043
+ url,
1044
+ username: this.#webdavUser.trim(),
1045
+ password: this.#webdavPass
1046
+ };
1047
+ this.#flow?.submitWebdav(config);
1048
+ }
1049
+ };
1050
+
1051
+ // src/widgets/share.ts
1052
+ var EN2 = {
1053
+ "share.title": "Sharing",
1054
+ "share.create.read": "Create a view link",
1055
+ "share.create.write": "Create an edit link",
1056
+ "share.creating": "Creating...",
1057
+ "share.level.read": "Can view",
1058
+ "share.level.write": "Can edit",
1059
+ "share.copy": "Copy",
1060
+ "share.copied": "Copied",
1061
+ "share.revoke": "Revoke",
1062
+ "share.members": "People with access",
1063
+ "share.member.you": "you",
1064
+ "share.member.owner": "owner",
1065
+ "share.remove": "Remove",
1066
+ "share.stop": "Stop sharing",
1067
+ "share.empty": "Nobody else has access yet.",
1068
+ "share.stale": "Connection hiccup: this view may be behind.",
1069
+ "error.generic": "That did not work. Check the connection and try again.",
1070
+ "error.targetUnavailable": "The share service did not answer. Try again in a moment."
1071
+ };
1072
+ var isOff = (v) => v != null && ["off", "false", "no", "0"].includes(v.trim().toLowerCase());
1073
+ var SelfstoreShareElement = class extends FlowWidget {
1074
+ static get observedAttributes() {
1075
+ return ["levels", "with-create", "with-members"];
1076
+ }
1077
+ #engine = null;
1078
+ #flow = null;
1079
+ #options = {};
1080
+ #levels = ["read", "write"];
1081
+ #withCreate = true;
1082
+ #withMembers = true;
1083
+ #confirmAction = null;
1084
+ #qrProvider = null;
1085
+ #qr = /* @__PURE__ */ new Map();
1086
+ #qrPending = /* @__PURE__ */ new Set();
1087
+ #copiedId = null;
1088
+ #creating = null;
1089
+ defaults() {
1090
+ return EN2;
1091
+ }
1092
+ /** The app-side transport of links and memberships (see ShareEngine). */
1093
+ get engine() {
1094
+ return this.#engine;
1095
+ }
1096
+ set engine(v) {
1097
+ this.#engine = v;
1098
+ this.wire();
1099
+ }
1100
+ /** Flow options (deadlineMs), for engines whose legs outgrow the default
1101
+ * 30s guard (a large first upload, a slow relay). */
1102
+ get options() {
1103
+ return this.#options;
1104
+ }
1105
+ set options(v) {
1106
+ this.#options = v ?? {};
1107
+ this.wire();
1108
+ }
1109
+ /** Which levels the panel OFFERS to create (['read', 'write'] by default) -
1110
+ * one create button per level without a live link. Also the `levels`
1111
+ * attribute, comma-separated: levels="read". Links the engine lists
1112
+ * render whatever this says. */
1113
+ get levels() {
1114
+ return [...this.#levels];
1115
+ }
1116
+ set levels(v) {
1117
+ const clean = [...new Set((v ?? []).filter((l) => l === "read" || l === "write"))];
1118
+ this.#levels = clean.length > 0 ? clean : ["read", "write"];
1119
+ this.rerender();
1120
+ }
1121
+ /** Render the create buttons (default). `with-create="off"` hides them: the
1122
+ * panel becomes list-and-revoke only. */
1123
+ get withCreate() {
1124
+ return this.#withCreate;
1125
+ }
1126
+ set withCreate(v) {
1127
+ this.#withCreate = v !== false;
1128
+ this.rerender();
1129
+ }
1130
+ /** Render the members section (default). `with-members="off"` hides it:
1131
+ * a links-only panel. */
1132
+ get withMembers() {
1133
+ return this.#withMembers;
1134
+ }
1135
+ set withMembers(v) {
1136
+ this.#withMembers = v !== false;
1137
+ this.rerender();
1138
+ }
1139
+ /** Host veto over the destructive actions: called before revoke, remove or
1140
+ * stop runs; answer (or resolve) false to keep things as they are -
1141
+ * window.confirm fits as-is. Default: everything proceeds. */
1142
+ get confirmAction() {
1143
+ return this.#confirmAction;
1144
+ }
1145
+ set confirmAction(v) {
1146
+ this.#confirmAction = v ?? null;
1147
+ }
1148
+ /** Turn a link URL into an image source (a data URL fits): when set, each
1149
+ * link card renders it as an img part="qr". Injected so the element ships
1150
+ * no QR dependency. */
1151
+ get qrProvider() {
1152
+ return this.#qrProvider;
1153
+ }
1154
+ set qrProvider(v) {
1155
+ this.#qrProvider = v ?? null;
1156
+ this.#qr.clear();
1157
+ this.#qrPending.clear();
1158
+ this.rerender();
1159
+ }
1160
+ get flow() {
1161
+ return this.#flow;
1162
+ }
1163
+ attributeChangedCallback(name, _old, value) {
1164
+ if (name === "levels") {
1165
+ this.levels = (value ?? "").split(",").map((s) => s.trim());
1166
+ }
1167
+ if (name === "with-create") this.withCreate = value == null || !isOff(value);
1168
+ if (name === "with-members") this.withMembers = value == null || !isOff(value);
1169
+ }
1170
+ connectedCallback() {
1171
+ this.wire();
1172
+ }
1173
+ wire() {
1174
+ if (!this.isConnected || !this.#engine) return;
1175
+ this.unsub?.();
1176
+ this.#flow = shareFlow(this.#engine, this.#options);
1177
+ this.unsub = this.#flow.subscribe(() => this.rerender());
1178
+ }
1179
+ view(into) {
1180
+ const flow = this.#flow;
1181
+ if (!flow) return;
1182
+ const s = flow.snapshot;
1183
+ put(into, this.heading("title", "share.title"));
1184
+ if (s.stale) into.append(h("div", { part: "hint" }, this.t("share.stale")));
1185
+ if (s.error && !s.stale) {
1186
+ into.append(h("div", { part: "error-note" }, this.errorText(s.error.labelKey)));
1187
+ }
1188
+ if (this.#withCreate) {
1189
+ const missing = this.#levels.filter((l) => !s.links.some((k) => k.level === l));
1190
+ if (missing.length > 0) {
1191
+ into.append(
1192
+ h(
1193
+ "div",
1194
+ { part: "row" },
1195
+ ...missing.map(
1196
+ (level) => h(
1197
+ "button",
1198
+ {
1199
+ part: "button button-primary",
1200
+ "data-action": `create-${level}`,
1201
+ disabled: s.busy !== null,
1202
+ onclick: () => this.create(level)
1203
+ },
1204
+ this.t(
1205
+ s.busy === "create" && this.#creating === level ? "share.creating" : `share.create.${level}`
1206
+ )
1207
+ )
1208
+ )
1209
+ )
1210
+ );
1211
+ }
1212
+ }
1213
+ if (s.links.length > 0) {
1214
+ const list = h("ul", { part: "list" });
1215
+ for (const link of s.links) {
1216
+ const qr = this.qrFor(link.url);
1217
+ list.append(
1218
+ h(
1219
+ "li",
1220
+ { part: "card", "data-link": link.id },
1221
+ qr ? h("img", { part: "qr", src: qr, alt: "" }) : null,
1222
+ h(
1223
+ "div",
1224
+ { style: "flex:1;min-width:0" },
1225
+ h("div", { part: "title" }, this.t(`share.level.${link.level}`)),
1226
+ h("div", { part: "sub", style: "word-break:break-all" }, link.url)
1227
+ ),
1228
+ h(
1229
+ "button",
1230
+ { part: "button", "data-action": "copy", onclick: () => this.copy(link.id, link.url) },
1231
+ this.t(this.#copiedId === link.id ? "share.copied" : "share.copy")
1232
+ ),
1233
+ h(
1234
+ "button",
1235
+ { part: "button button-danger", "data-action": "revoke", disabled: s.busy !== null, onclick: () => this.revoke(link.id) },
1236
+ this.t("share.revoke")
1237
+ )
1238
+ )
1239
+ );
1240
+ }
1241
+ into.append(list);
1242
+ }
1243
+ if (this.#withMembers) {
1244
+ put(into, this.heading("sub", "share.members"));
1245
+ if (s.members.length === 0) {
1246
+ into.append(h("div", { part: "hint" }, this.t("share.empty")));
1247
+ } else {
1248
+ const members = h("ul", { part: "list" });
1249
+ for (const m of s.members) {
1250
+ const badges = [];
1251
+ if (m.self) badges.push(this.t("share.member.you"));
1252
+ if (m.owner) badges.push(this.t("share.member.owner"));
1253
+ members.append(
1254
+ h(
1255
+ "li",
1256
+ { part: "card", "data-member": m.id },
1257
+ h(
1258
+ "div",
1259
+ { style: "flex:1" },
1260
+ h("div", { part: "title" }, m.label || m.id),
1261
+ badges.length ? h("div", { part: "sub" }, badges.join(" - ")) : null
1262
+ ),
1263
+ s.canRemoveMembers && !m.self ? h(
1264
+ "button",
1265
+ { part: "button button-danger", "data-action": "remove", disabled: s.busy !== null, onclick: () => this.removeRow(m.id) },
1266
+ this.t("share.remove")
1267
+ ) : null
1268
+ )
1269
+ );
1270
+ }
1271
+ into.append(members);
1272
+ }
1273
+ }
1274
+ if (s.canRevokeAll && s.members.some((m) => !m.self)) {
1275
+ into.append(
1276
+ h(
1277
+ "div",
1278
+ { part: "footer", style: "text-align:right" },
1279
+ h(
1280
+ "button",
1281
+ { part: "link link-danger", "data-action": "stop", disabled: s.busy !== null, onclick: () => this.stop() },
1282
+ this.t("share.stop")
1283
+ )
1284
+ )
1285
+ );
1286
+ }
1287
+ }
1288
+ create(level) {
1289
+ this.#creating = level;
1290
+ void this.#flow?.createLink({ level }).then((link) => {
1291
+ this.#creating = null;
1292
+ if (link) this.emit("selfstore-link-created", { link });
1293
+ this.rerender();
1294
+ });
1295
+ }
1296
+ /** Run a destructive action through the host's veto (default: proceed).
1297
+ * A throwing hook reads as "no": destruction needs a clear yes. */
1298
+ guarded(action, run) {
1299
+ const gate = this.#confirmAction;
1300
+ if (!gate) {
1301
+ run();
1302
+ return;
1303
+ }
1304
+ void Promise.resolve().then(() => gate(action)).then((ok) => {
1305
+ if (ok) run();
1306
+ }).catch(() => void 0);
1307
+ }
1308
+ revoke(id) {
1309
+ this.guarded({ type: "revoke", id }, () => void this.#flow?.revokeLink(id));
1310
+ }
1311
+ removeRow(id) {
1312
+ this.guarded({ type: "remove", id }, () => void this.#flow?.removeMember(id));
1313
+ }
1314
+ stop() {
1315
+ this.guarded({ type: "stop" }, () => {
1316
+ void this.#flow?.revokeAll().then((ok) => {
1317
+ if (ok) this.emit("selfstore-share-stopped");
1318
+ });
1319
+ });
1320
+ }
1321
+ qrFor(url) {
1322
+ const provider = this.#qrProvider;
1323
+ if (!provider) return null;
1324
+ const got = this.#qr.get(url);
1325
+ if (got) return got;
1326
+ if (!this.#qrPending.has(url)) {
1327
+ this.#qrPending.add(url);
1328
+ void provider(url).then((src) => {
1329
+ if (this.#qrProvider !== provider) return;
1330
+ this.#qr.set(url, src);
1331
+ this.rerender();
1332
+ }).catch(() => void 0).finally(() => this.#qrPending.delete(url));
1333
+ }
1334
+ return null;
1335
+ }
1336
+ copy(id, url) {
1337
+ this.#copiedId = id;
1338
+ this.emit("selfstore-link-copied", { url });
1339
+ void navigator.clipboard?.writeText(url).catch(() => void 0);
1340
+ this.rerender();
1341
+ setTimeout(() => {
1342
+ if (this.#copiedId !== id) return;
1343
+ this.#copiedId = null;
1344
+ this.rerender();
1345
+ }, 1500);
1346
+ }
1347
+ };
1348
+
1349
+ // src/widgets/join.ts
1350
+ var EN3 = {
1351
+ "join.previewing": "Reading the invitation...",
1352
+ "join.title": "You are invited",
1353
+ "join.from": "Shared by {from}",
1354
+ "join.level.read": "You will be able to view it.",
1355
+ "join.level.write": "You will be able to view and edit it.",
1356
+ "join.accept": "Join",
1357
+ "join.joining": "Joining...",
1358
+ "join.joined": "You are in. The shared data now syncs on this device.",
1359
+ "join.mismatch": "This device already follows another share. Leave it first, or use another profile.",
1360
+ "join.noInvite": "This invitation is spent, or meant for another account.",
1361
+ "join.switchAccount": "Use another account",
1362
+ "join.retry": "Try again",
1363
+ "error.generic": "That did not work. Check the connection and try again.",
1364
+ "error.targetUnavailable": "The invitation could not be read. Try again in a moment."
1365
+ };
1366
+ var SelfstoreJoinElement = class extends FlowWidget {
1367
+ static get observedAttributes() {
1368
+ return ["link", "variant"];
1369
+ }
1370
+ #engine = null;
1371
+ #link = null;
1372
+ #flow = null;
1373
+ #variant = "card";
1374
+ #options = {};
1375
+ defaults() {
1376
+ return EN3;
1377
+ }
1378
+ get engine() {
1379
+ return this.#engine;
1380
+ }
1381
+ set engine(v) {
1382
+ this.#engine = v;
1383
+ this.wire();
1384
+ }
1385
+ /** The invitation. Also settable as the `link` attribute. */
1386
+ get link() {
1387
+ return this.#link;
1388
+ }
1389
+ set link(v) {
1390
+ this.#link = v;
1391
+ this.wire();
1392
+ }
1393
+ /** Flow options (deadlineMs). Set it when the engine's join opens a popup:
1394
+ * a generous deadline keeps the network guard without timing out a human
1395
+ * who is reading an account chooser. */
1396
+ get options() {
1397
+ return this.#options;
1398
+ }
1399
+ set options(v) {
1400
+ this.#options = v ?? {};
1401
+ this.wire();
1402
+ }
1403
+ /** 'card' (default): the full invitation card - who shared, what level.
1404
+ * 'banner': one line - label, join, account switch - for a host page that
1405
+ * renders its own preview around the widget. Also the `variant` attribute. */
1406
+ get variant() {
1407
+ return this.#variant;
1408
+ }
1409
+ set variant(v) {
1410
+ this.#variant = v === "banner" ? "banner" : "card";
1411
+ this.rerender();
1412
+ }
1413
+ get flow() {
1414
+ return this.#flow;
1415
+ }
1416
+ attributeChangedCallback(name, _old, value) {
1417
+ if (name === "link") this.link = value;
1418
+ if (name === "variant") this.variant = value ?? "card";
1419
+ }
1420
+ connectedCallback() {
1421
+ if (!this.#link && this.hasAttribute("link")) this.#link = this.getAttribute("link");
1422
+ if (this.hasAttribute("variant")) {
1423
+ this.#variant = this.getAttribute("variant") === "banner" ? "banner" : "card";
1424
+ }
1425
+ this.wire();
1426
+ }
1427
+ wire() {
1428
+ if (!this.isConnected || !this.#engine || !this.#link) return;
1429
+ this.unsub?.();
1430
+ this.#flow = joinFlow(this.#link, this.#engine, this.#options);
1431
+ let lastStep = null;
1432
+ this.unsub = this.#flow.subscribe((snap) => {
1433
+ if (snap.step !== lastStep) {
1434
+ if (snap.step === "joined") this.emit("selfstore-joined", { outcome: "joined" });
1435
+ if (snap.step === "mismatch" || snap.step === "no-invite") {
1436
+ this.emit("selfstore-join-refused", { outcome: snap.step });
1437
+ }
1438
+ if (snap.step === "ready" && lastStep === "joining") {
1439
+ this.emit("selfstore-cancelled");
1440
+ }
1441
+ lastStep = snap.step;
1442
+ }
1443
+ this.rerender();
1444
+ });
1445
+ }
1446
+ view(into) {
1447
+ const flow = this.#flow;
1448
+ if (!flow) return;
1449
+ const s = flow.snapshot;
1450
+ if (s.step === "previewing" || s.step === "joining") {
1451
+ into.append(
1452
+ h(
1453
+ "div",
1454
+ { part: "status" },
1455
+ h("span", { part: "spinner", "aria-hidden": "true" }),
1456
+ h("div", { part: "title" }, this.t(s.step === "joining" ? "join.joining" : "join.previewing"))
1457
+ )
1458
+ );
1459
+ return;
1460
+ }
1461
+ if (s.step === "ready") {
1462
+ const p = s.preview;
1463
+ if (this.#variant === "banner") {
1464
+ put(
1465
+ into,
1466
+ h(
1467
+ "div",
1468
+ { part: "row banner" },
1469
+ h("div", { part: "title", style: "flex:1;min-width:0" }, p?.label || this.t("join.title")),
1470
+ h("button", { part: "button button-primary", "data-action": "accept", onclick: () => flow.accept() }, this.t("join.accept")),
1471
+ s.canSwitchAccount ? h("button", { part: "link", "data-action": "switch", onclick: () => flow.switchAccount() }, this.t("join.switchAccount")) : null
1472
+ )
1473
+ );
1474
+ return;
1475
+ }
1476
+ put(
1477
+ into,
1478
+ h("div", { part: "title" }, p?.label || this.t("join.title")),
1479
+ p?.from ? h("div", { part: "sub" }, this.t("join.from").replace("{from}", p.from)) : null,
1480
+ p?.level ? h("div", { part: "hint" }, this.t(`join.level.${p.level}`)) : null,
1481
+ h(
1482
+ "div",
1483
+ { part: "row" },
1484
+ h("button", { part: "button button-primary", "data-action": "accept", onclick: () => flow.accept() }, this.t("join.accept")),
1485
+ s.canSwitchAccount ? h("button", { part: "link", "data-action": "switch", onclick: () => flow.switchAccount() }, this.t("join.switchAccount")) : null
1486
+ )
1487
+ );
1488
+ return;
1489
+ }
1490
+ if (s.step === "joined") {
1491
+ into.append(h("div", { part: "status status-ok" }, h("div", { part: "title" }, this.t("join.joined"))));
1492
+ return;
1493
+ }
1494
+ if (s.step === "mismatch" || s.step === "no-invite") {
1495
+ into.append(
1496
+ h("div", { part: "status" }, h("div", { part: "title" }, this.t(s.step === "mismatch" ? "join.mismatch" : "join.noInvite")))
1497
+ );
1498
+ if (s.step === "no-invite" && s.canSwitchAccount) {
1499
+ into.append(
1500
+ h("div", { part: "row" }, h("button", { part: "button", "data-action": "switch", onclick: () => flow.switchAccount() }, this.t("join.switchAccount")))
1501
+ );
1502
+ }
1503
+ return;
1504
+ }
1505
+ into.append(
1506
+ h("div", { part: "status status-error" }, h("div", { part: "title" }, this.errorText(s.error?.labelKey))),
1507
+ h(
1508
+ "div",
1509
+ { part: "row" },
1510
+ h("button", { part: "button", onclick: () => flow.retry() }, this.t("join.retry")),
1511
+ s.canSwitchAccount ? h("button", { part: "link", "data-action": "switch", onclick: () => flow.switchAccount() }, this.t("join.switchAccount")) : null
1512
+ )
1513
+ );
1514
+ }
1515
+ };
1516
+
1517
+ // src/widgets/status.ts
1518
+ var EN4 = {
1519
+ "status.ephemeral": "Nothing is saved",
1520
+ "status.cacheOnly": "Only on this device",
1521
+ "status.saving": "Saving...",
1522
+ "status.saved": "Saved",
1523
+ "status.needsAttention": "Reconnect to continue",
1524
+ "status.locked": "Locked",
1525
+ "status.pendingDownload": "Changes to download",
1526
+ "status.action.choose-destination": "Choose a destination",
1527
+ "status.action.download": "Download",
1528
+ "status.action.reconnect": "Reconnect",
1529
+ "status.action.unlock": "Unlock"
1530
+ };
1531
+ var SelfstoreStatusElement = class extends FlowWidget {
1532
+ static get observedAttributes() {
1533
+ return ["variant"];
1534
+ }
1535
+ #store = null;
1536
+ #variant = "row";
1537
+ #icons = {};
1538
+ defaults() {
1539
+ return EN4;
1540
+ }
1541
+ /** The simple store (anything exposing `flowHost`), or a hand-built FlowHost. */
1542
+ get store() {
1543
+ return this.#store;
1544
+ }
1545
+ set store(v) {
1546
+ this.#store = v;
1547
+ this.wire();
1548
+ }
1549
+ /** 'row' (dot + text + action) or 'dot' (the dot alone). Also the attribute. */
1550
+ get variant() {
1551
+ return this.#variant;
1552
+ }
1553
+ set variant(v) {
1554
+ this.#variant = v === "dot" ? "dot" : "row";
1555
+ this.rerender();
1556
+ }
1557
+ /** Optional icon per target kind (e.g. { drive: url, file: url }), shown
1558
+ * before the text in the row variant. Same contract as the connect cards. */
1559
+ get icons() {
1560
+ return this.#icons;
1561
+ }
1562
+ set icons(v) {
1563
+ this.#icons = v ?? {};
1564
+ this.rerender();
1565
+ }
1566
+ attributeChangedCallback(name, _old, value) {
1567
+ if (name === "variant") this.variant = value;
1568
+ }
1569
+ connectedCallback() {
1570
+ if (this.hasAttribute("variant")) this.variant = this.getAttribute("variant");
1571
+ this.wire();
1572
+ }
1573
+ host() {
1574
+ const s = this.#store;
1575
+ if (!s) return null;
1576
+ return "flowHost" in s ? s.flowHost : s;
1577
+ }
1578
+ wire() {
1579
+ if (!this.isConnected) return;
1580
+ this.unsub?.();
1581
+ this.unsub = null;
1582
+ const host = this.host();
1583
+ if (!host) return;
1584
+ this.unsub = host.engine.subscribe(() => this.rerender());
1585
+ this.rerender();
1586
+ }
1587
+ view(into) {
1588
+ const host = this.host();
1589
+ if (!host) return;
1590
+ const { status, targetKind, label } = host.engine.state;
1591
+ const text = this.t(status.labelKey);
1592
+ const dot = h("span", {
1593
+ part: `status-dot sev-${status.severity}`,
1594
+ "aria-hidden": "true"
1595
+ });
1596
+ if (this.#variant === "dot") {
1597
+ into.append(
1598
+ h(
1599
+ "button",
1600
+ {
1601
+ part: "dot-button",
1602
+ "aria-label": text,
1603
+ title: text,
1604
+ onclick: () => this.emit("selfstore-status-action", { action: status.action ?? null })
1605
+ },
1606
+ dot
1607
+ )
1608
+ );
1609
+ return;
1610
+ }
1611
+ const icon = this.#icons[targetKind];
1612
+ const where = this.t(`status.where.${targetKind}`);
1613
+ const sub = where !== `status.where.${targetKind}` ? where : label ?? "";
1614
+ put(
1615
+ into,
1616
+ h(
1617
+ "div",
1618
+ { part: "row status-row" },
1619
+ dot,
1620
+ icon ? h("img", { part: "icon", src: icon, alt: "" }) : null,
1621
+ h(
1622
+ "div",
1623
+ {},
1624
+ h("div", { part: "title" }, text),
1625
+ sub ? h("div", { part: "sub" }, sub) : null
1626
+ ),
1627
+ status.actionable && status.action ? h(
1628
+ "button",
1629
+ {
1630
+ part: "button status-action",
1631
+ onclick: () => this.emit("selfstore-status-action", { action: status.action })
1632
+ },
1633
+ this.t(`status.action.${status.action}`)
1634
+ ) : null
1635
+ )
1636
+ );
1637
+ }
1638
+ };
1639
+
1640
+ // src/widgets/backups.ts
1641
+ var EN5 = {
1642
+ "backups.mine.heading": "My backups",
1643
+ "backups.mine.personal": "Main backup",
1644
+ "backups.active": "Active",
1645
+ "backups.pill.encrypted": "Encrypted",
1646
+ "backups.pill.plain": "Not encrypted",
1647
+ "backups.pill.shared": "Shared",
1648
+ "backups.pill.private": "Not shared",
1649
+ "backups.modified": "modified {when}",
1650
+ "backups.replaceHint": "Opening replaces the data on this device.",
1651
+ "backups.loading": "Opening...",
1652
+ "backups.menu": "Backup actions",
1653
+ "backups.open": "Open",
1654
+ "backups.encrypt": "Encrypt",
1655
+ "backups.share": "Share",
1656
+ "backups.rename": "Rename",
1657
+ "backups.delete": "Delete",
1658
+ "backups.new": "+ New backup",
1659
+ "backups.newTitle": "New backup",
1660
+ "backups.startsBlank": "It starts empty.",
1661
+ "backups.namePh": "Its name",
1662
+ "backups.willBe": "File: {file}",
1663
+ "backups.taken": "This name is already taken.",
1664
+ "backups.create": "Create",
1665
+ "backups.renameTitle": "Rename this backup",
1666
+ "backups.pwHint": "This backup is protected: enter its password.",
1667
+ "backups.pwShow": "Show the password",
1668
+ "backups.pwHide": "Hide the password",
1669
+ "backups.cancel": "Cancel",
1670
+ "backups.shared.heading": "Shared with me",
1671
+ "backups.shared.by": "Shared by {who}",
1672
+ "backups.shared.leave": "Leave",
1673
+ "backups.replica.add": "Backup copy",
1674
+ "backups.replica.title": "Backup copy",
1675
+ "backups.replica.intro": "The same encrypted file, also written to a second destination. If one fails, the other remains.",
1676
+ "backups.replica.dest.drive": "Google Drive",
1677
+ "backups.replica.dest.file": "A file on this device",
1678
+ "backups.replica.dest.webdav": "A WebDAV server",
1679
+ "backups.replica.dest.s3": "An S3 bucket",
1680
+ "backups.replica.card": "Backup copy - {label}",
1681
+ "backups.replica.uptodate": "Up to date, {when}",
1682
+ "backups.replica.pending": "First copy at the next save",
1683
+ "backups.replica.error": "Unreachable - will retry at the next save",
1684
+ "backups.replica.remove": "Remove",
1685
+ "backups.replica.save": "Add the copy",
1686
+ "backups.replica.webdav.url": "File URL on the server (https://host:8443/dav/file.zip)",
1687
+ "backups.replica.webdav.help": "The full URL of the backup file, including a custom port if any. The server must allow this app to reach it (CORS) - some hosted drives do not, and will not connect from a browser.",
1688
+ "backups.replica.webdav.user": "Username",
1689
+ "backups.replica.webdav.password": "Password",
1690
+ "backups.replica.s3.endpoint": "Endpoint URL",
1691
+ "backups.replica.s3.region": "Region",
1692
+ "backups.replica.s3.bucket": "Bucket",
1693
+ "backups.replica.s3.key": "Object key (file name)",
1694
+ "backups.replica.s3.accessKeyId": "Access key ID",
1695
+ "backups.replica.s3.secret": "Secret access key",
1696
+ "error.generic": "That did not work. Check the connection and try again."
1697
+ };
1698
+ var isOff2 = (v) => v != null && ["off", "false", "no", "0"].includes(v.trim().toLowerCase());
1699
+ var EYE_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>';
1700
+ var EYE_SHUT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 3 18 18"/><path d="M10.6 5.1A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a17.4 17.4 0 0 1-3.4 4.4M6.6 6.6A17 17 0 0 0 2 12s3.5 7 10 7a10.7 10.7 0 0 0 5.4-1.4"/></svg>';
1701
+ var SelfstoreBackupsElement = class extends FlowWidget {
1702
+ static get observedAttributes() {
1703
+ return ["with-create", "with-rename", "with-delete", "with-open", "with-encrypt", "with-share", "with-shared"];
1704
+ }
1705
+ #manager = null;
1706
+ #rows = null;
1707
+ #lastActive = void 0;
1708
+ #listing = false;
1709
+ #busy = false;
1710
+ #openBusy = null;
1711
+ #menuFor = null;
1712
+ #pwFor = null;
1713
+ #pw = "";
1714
+ #pwShown = false;
1715
+ #renameFor = null;
1716
+ #renameLabel = "";
1717
+ #createOpen = false;
1718
+ #createLabel = "";
1719
+ #members = [];
1720
+ #replicaFlow = null;
1721
+ #replicaSnap = null;
1722
+ #repWebdav = { url: "", username: "", password: "" };
1723
+ #repS3 = { endpoint: "", region: "", bucket: "", key: "", accessKeyId: "", secretAccessKey: "" };
1724
+ #activeEncrypted = null;
1725
+ #activeShared = null;
1726
+ #confirmAction = null;
1727
+ #withCreate = true;
1728
+ #withRename = true;
1729
+ #withDelete = true;
1730
+ #withOpen = true;
1731
+ #withEncrypt = true;
1732
+ #withShare = true;
1733
+ #withShared = true;
1734
+ defaults() {
1735
+ return EN5;
1736
+ }
1737
+ /** The headless manager this panel drives (see 'selfstore/backups'). */
1738
+ get manager() {
1739
+ return this.#manager;
1740
+ }
1741
+ set manager(v) {
1742
+ this.#manager = v;
1743
+ this.wire();
1744
+ }
1745
+ /** The households this device is a member of (one entry per share, or a
1746
+ * single object for one): each renders in "Shared with me" labeled by who
1747
+ * shares it, removal = leaving (host performs it on
1748
+ * 'selfstore-backups-leave', whose detail names the entry's fileId). With
1749
+ * `fileId` (the share's dedicated wallet), that registry row IS the
1750
+ * member row: active it wears the badge, inactive it opens like any
1751
+ * shared silo - and its one menu action is leaving, never a mere forget. */
1752
+ get member() {
1753
+ return this.#members.length > 0 ? this.#members : null;
1754
+ }
1755
+ set member(v) {
1756
+ this.#members = v == null ? [] : Array.isArray(v) ? v : [v];
1757
+ this.rerender();
1758
+ }
1759
+ /** Live state of the ATTACHED backup, from the host's store: the active
1760
+ * row's pills prefer these over the learned memories (which may lag). */
1761
+ get activeEncrypted() {
1762
+ return this.#activeEncrypted;
1763
+ }
1764
+ set activeEncrypted(v) {
1765
+ this.#activeEncrypted = v;
1766
+ this.rerender();
1767
+ }
1768
+ get activeShared() {
1769
+ return this.#activeShared;
1770
+ }
1771
+ set activeShared(v) {
1772
+ this.#activeShared = v;
1773
+ this.rerender();
1774
+ }
1775
+ /** Host veto over the destructive gestures (delete / forget / leave):
1776
+ * answer (or resolve) false to keep things as they are - window.confirm
1777
+ * fits as-is. Default: everything proceeds. */
1778
+ get confirmAction() {
1779
+ return this.#confirmAction;
1780
+ }
1781
+ set confirmAction(v) {
1782
+ this.#confirmAction = v ?? null;
1783
+ }
1784
+ get withCreate() {
1785
+ return this.#withCreate;
1786
+ }
1787
+ set withCreate(v) {
1788
+ this.#withCreate = v !== false;
1789
+ this.rerender();
1790
+ }
1791
+ get withRename() {
1792
+ return this.#withRename;
1793
+ }
1794
+ set withRename(v) {
1795
+ this.#withRename = v !== false;
1796
+ this.rerender();
1797
+ }
1798
+ get withDelete() {
1799
+ return this.#withDelete;
1800
+ }
1801
+ set withDelete(v) {
1802
+ this.#withDelete = v !== false;
1803
+ this.rerender();
1804
+ }
1805
+ get withOpen() {
1806
+ return this.#withOpen;
1807
+ }
1808
+ set withOpen(v) {
1809
+ this.#withOpen = v !== false;
1810
+ this.rerender();
1811
+ }
1812
+ get withEncrypt() {
1813
+ return this.#withEncrypt;
1814
+ }
1815
+ set withEncrypt(v) {
1816
+ this.#withEncrypt = v !== false;
1817
+ this.rerender();
1818
+ }
1819
+ get withShare() {
1820
+ return this.#withShare;
1821
+ }
1822
+ set withShare(v) {
1823
+ this.#withShare = v !== false;
1824
+ this.rerender();
1825
+ }
1826
+ get withShared() {
1827
+ return this.#withShared;
1828
+ }
1829
+ set withShared(v) {
1830
+ this.#withShared = v !== false;
1831
+ this.rerender();
1832
+ }
1833
+ attributeChangedCallback(name, _old, value) {
1834
+ const on = value == null || !isOff2(value);
1835
+ if (name === "with-create") this.withCreate = on;
1836
+ if (name === "with-rename") this.withRename = on;
1837
+ if (name === "with-delete") this.withDelete = on;
1838
+ if (name === "with-open") this.withOpen = on;
1839
+ if (name === "with-encrypt") this.withEncrypt = on;
1840
+ if (name === "with-share") this.withShare = on;
1841
+ if (name === "with-shared") this.withShared = on;
1842
+ }
1843
+ connectedCallback() {
1844
+ this.wire();
1845
+ }
1846
+ /** Re-list the rows from the destination (the host calls this after a
1847
+ * gesture of its own moved things - a leave, an external change). */
1848
+ refresh() {
1849
+ void this.loadRows();
1850
+ }
1851
+ wire() {
1852
+ if (!this.isConnected || !this.#manager) return;
1853
+ this.unsub?.();
1854
+ const manager = this.#manager;
1855
+ const unsubManager = manager.subscribe((snap) => {
1856
+ if (snap.activeFileId !== this.#lastActive) {
1857
+ this.#lastActive = snap.activeFileId;
1858
+ void this.loadRows();
1859
+ }
1860
+ this.rerender();
1861
+ });
1862
+ const unsubReplica = this.#replicaFlow?.subscribe((snap) => {
1863
+ this.#replicaSnap = snap;
1864
+ this.rerender();
1865
+ });
1866
+ this.unsub = () => {
1867
+ unsubManager();
1868
+ unsubReplica?.();
1869
+ };
1870
+ void manager.hydrate().then(() => this.loadRows());
1871
+ }
1872
+ /** The backup-copy journey (see 'selfstore/flows' replicaFlow). Setting it
1873
+ * adds a menu entry on the active backup's card and renders the copy
1874
+ * cards under it; without it the panel is byte-identical to before. The
1875
+ * widget restores a recorded copy by itself. */
1876
+ get replica() {
1877
+ return this.#replicaFlow;
1878
+ }
1879
+ set replica(v) {
1880
+ this.#replicaFlow = v;
1881
+ this.#replicaSnap = v?.snapshot ?? null;
1882
+ if (v) void v.restore();
1883
+ this.wire();
1884
+ }
1885
+ /** One destination listing, then the lazy encryption probes (each learned
1886
+ * bit repaints its row). A transient failure keeps the last good list. */
1887
+ async loadRows() {
1888
+ const manager = this.#manager;
1889
+ if (!manager || this.#listing) return;
1890
+ this.#listing = true;
1891
+ try {
1892
+ this.#rows = await manager.list();
1893
+ this.rerender();
1894
+ for (const r of this.#rows ?? []) {
1895
+ if (r.encrypted !== null) continue;
1896
+ const enc = await manager.probeEncryption(r.fileId);
1897
+ if (enc === null || this.#rows === null) continue;
1898
+ this.#rows = this.#rows.map((x) => x.fileId === r.fileId ? { ...x, encrypted: enc } : x);
1899
+ this.rerender();
1900
+ }
1901
+ } catch {
1902
+ this.#rows = this.#rows ?? [];
1903
+ this.rerender();
1904
+ } finally {
1905
+ this.#listing = false;
1906
+ }
1907
+ }
1908
+ /** t() with {placeholders}. */
1909
+ ts(key, params) {
1910
+ return this.t(key).replace(/\{(\w+)\}/g, (_, k) => params[k] ?? "");
1911
+ }
1912
+ label(row) {
1913
+ return row.label ?? this.t("backups.mine.personal");
1914
+ }
1915
+ view(into) {
1916
+ const manager = this.#manager;
1917
+ if (!manager) return;
1918
+ const snap = manager.snapshot;
1919
+ const rows = this.#rows ?? [];
1920
+ const active = snap.activeFileId;
1921
+ const mine = snap.joined ? rows.filter((r) => r.fileId !== active) : rows;
1922
+ if (this.#rows !== null) put(into, this.heading("sub heading", "backups.mine.heading"));
1923
+ const list = h("ul", { part: "list" });
1924
+ for (const r of mine) {
1925
+ const isActive = r.fileId === active;
1926
+ if (this.#pwFor === r.fileId) list.append(this.passwordCard(r));
1927
+ else if (this.#renameFor?.fileId === r.fileId) list.append(this.renameCard(r));
1928
+ else list.append(this.rowCard(r, isActive, isActive ? this.replicaAttachedLine() : null));
1929
+ if (isActive) for (const el of this.replicaSetup()) list.append(el);
1930
+ }
1931
+ if (list.childElementCount > 0) into.append(list);
1932
+ if (this.#withCreate) {
1933
+ if (this.#createOpen) into.append(this.createCard());
1934
+ else {
1935
+ into.append(
1936
+ h(
1937
+ "button",
1938
+ {
1939
+ part: "button new-button",
1940
+ "data-action": "new",
1941
+ disabled: this.#busy,
1942
+ onclick: () => {
1943
+ this.#createOpen = true;
1944
+ this.#createLabel = "";
1945
+ this.rerender();
1946
+ }
1947
+ },
1948
+ this.t("backups.new")
1949
+ )
1950
+ );
1951
+ }
1952
+ }
1953
+ if (!this.#withShared) return;
1954
+ const members = this.#members;
1955
+ const badge = members.find((m) => m.fileId != null ? m.fileId === active : true) ?? null;
1956
+ const memberWhos = new Set(members.map((m) => m.by));
1957
+ const others = [];
1958
+ const seenForget = /* @__PURE__ */ new Set();
1959
+ for (const s of snap.registry.shared) {
1960
+ if (s.fileId === active && !snap.joined) continue;
1961
+ const member = members.some((m) => m.fileId === s.fileId);
1962
+ const who = s.ownerEmail ?? s.ownerName ?? "?";
1963
+ if (!member) {
1964
+ if (memberWhos.has(who) || seenForget.has(who)) continue;
1965
+ seenForget.add(who);
1966
+ }
1967
+ others.push({ fileId: s.fileId, who, member });
1968
+ }
1969
+ if (members.length === 0 && others.length === 0) return;
1970
+ put(into, this.heading("sub heading", "backups.shared.heading"));
1971
+ const shared = h("ul", { part: "list" });
1972
+ if (badge) {
1973
+ const activeRow = rows.find((r) => r.fileId === active) ?? null;
1974
+ const leaveAction = { type: "leave", fileId: badge.fileId ?? null, who: badge.by };
1975
+ shared.append(
1976
+ h(
1977
+ "li",
1978
+ { part: "card card-active", style: "position:relative;border-color:var(--_ok)", "data-member": "" },
1979
+ h(
1980
+ "div",
1981
+ { style: "flex:1;min-width:0" },
1982
+ h(
1983
+ "div",
1984
+ { part: "title" },
1985
+ badge.fileId != null || !activeRow ? this.ts("backups.shared.by", { who: badge.by }) : this.label(activeRow),
1986
+ h("span", { part: "tag" }, this.t("backups.active"))
1987
+ ),
1988
+ badge.fileId == null && activeRow ? h("div", { part: "sub" }, this.ts("backups.shared.by", { who: badge.by })) : null
1989
+ ),
1990
+ this.menuButton("member"),
1991
+ this.#menuFor === "member" ? this.menu(
1992
+ this.#withDelete ? this.menuItem(
1993
+ "leave",
1994
+ this.t("backups.shared.leave"),
1995
+ true,
1996
+ () => this.guarded(
1997
+ leaveAction,
1998
+ () => this.emit("selfstore-backups-leave", { fileId: leaveAction.fileId })
1999
+ )
2000
+ ) : null
2001
+ ) : null
2002
+ )
2003
+ );
2004
+ }
2005
+ for (const s of others) {
2006
+ if (badge && s.fileId === active) continue;
2007
+ const memberEntry = s.member;
2008
+ shared.append(
2009
+ h(
2010
+ "li",
2011
+ { part: "card", style: "position:relative", "data-shared": s.fileId },
2012
+ this.#withOpen ? h(
2013
+ "button",
2014
+ { part: "link open-shared", style: "flex:1;min-width:0;text-align:left;text-decoration:none", "data-action": "open-shared", disabled: this.#busy, onclick: () => this.open(s.fileId) },
2015
+ h("div", { part: "title" }, this.ts("backups.shared.by", { who: s.who })),
2016
+ h("div", { part: "sub" }, this.#openBusy === s.fileId ? this.t("backups.loading") : this.t("backups.replaceHint"))
2017
+ ) : h(
2018
+ "div",
2019
+ { style: "flex:1;min-width:0" },
2020
+ h("div", { part: "title" }, this.ts("backups.shared.by", { who: s.who })),
2021
+ h("div", { part: "sub" }, this.t("backups.replaceHint"))
2022
+ ),
2023
+ this.menuButton(s.fileId),
2024
+ this.#menuFor === s.fileId ? this.menu(
2025
+ this.#withDelete ? memberEntry ? this.menuItem(
2026
+ "leave",
2027
+ this.t("backups.shared.leave"),
2028
+ true,
2029
+ () => this.guarded(
2030
+ { type: "leave", fileId: s.fileId, who: s.who },
2031
+ () => this.emit("selfstore-backups-leave", { fileId: s.fileId })
2032
+ )
2033
+ ) : this.menuItem(
2034
+ "forget",
2035
+ this.t("backups.delete"),
2036
+ true,
2037
+ () => this.guarded({ type: "forget", fileId: s.fileId, who: s.who }, () => void this.forget(s.fileId))
2038
+ ) : null
2039
+ ) : null
2040
+ )
2041
+ );
2042
+ }
2043
+ if (shared.childElementCount > 0) into.append(shared);
2044
+ }
2045
+ // --- Cards ----------------------------------------------------------------
2046
+ pills(r, active) {
2047
+ const enc = active && this.#activeEncrypted !== null ? this.#activeEncrypted : r.encrypted;
2048
+ const shr = active && this.#activeShared !== null ? this.#activeShared : r.shared;
2049
+ const bits = [];
2050
+ if (enc !== null) {
2051
+ bits.push(
2052
+ h("span", { part: enc ? "pill pill-encrypted sev-ok" : "pill pill-plain", style: pillStyle }, this.t(enc ? "backups.pill.encrypted" : "backups.pill.plain"))
2053
+ );
2054
+ }
2055
+ if (shr !== null) {
2056
+ bits.push(
2057
+ h("span", { part: shr ? "pill pill-shared sev-warn" : "pill pill-private", style: pillStyle }, this.t(shr ? "backups.pill.shared" : "backups.pill.private"))
2058
+ );
2059
+ }
2060
+ if (r.modifiedAt) {
2061
+ bits.push(h("span", { part: "sub" }, this.ts("backups.modified", { when: new Date(r.modifiedAt).toLocaleDateString() })));
2062
+ }
2063
+ return bits;
2064
+ }
2065
+ rowCard(r, active, footer = null) {
2066
+ const body = h(
2067
+ "div",
2068
+ { style: "flex:1;min-width:0" },
2069
+ h("div", { part: "title" }, this.label(r), active ? h("span", { part: "tag" }, this.t("backups.active")) : null),
2070
+ h("div", { part: "row", style: "margin-top:0.2rem" }, ...this.pills(r, active)),
2071
+ footer
2072
+ );
2073
+ const opens = this.#withOpen && !active;
2074
+ const face = opens ? h(
2075
+ "button",
2076
+ { part: "link open-row", style: "flex:1;min-width:0;text-align:left;text-decoration:none", "data-action": "open", disabled: this.#busy, onclick: () => this.open(r.fileId) },
2077
+ this.#openBusy === r.fileId ? h("div", { part: "sub" }, this.t("backups.loading")) : body
2078
+ ) : body;
2079
+ return h(
2080
+ "li",
2081
+ { part: active ? "card card-active" : "card", style: active ? "position:relative;border-color:var(--_ok)" : "position:relative", "data-row": r.fileId },
2082
+ face,
2083
+ this.menuButton(r.fileId),
2084
+ this.#menuFor === r.fileId ? this.menu(
2085
+ opens ? this.menuItem("open", this.t("backups.open"), false, () => this.open(r.fileId)) : null,
2086
+ this.#withEncrypt ? this.menuItem("encrypt", this.t("backups.encrypt"), false, () => this.emit("selfstore-backups-encrypt", { fileId: r.fileId, label: r.label, active })) : null,
2087
+ this.#withShare ? this.menuItem("share", this.t("backups.share"), false, () => this.emit("selfstore-backups-share", { fileId: r.fileId, label: r.label, active })) : null,
2088
+ active && this.#replicaFlow && this.#replicaSnap?.step === "idle" && !this.#replicaSnap.replica ? this.menuItem("replica-add", this.t("backups.replica.add"), false, () => this.#replicaFlow?.open()) : null,
2089
+ this.#withRename ? this.menuItem("rename", this.t("backups.rename"), false, () => {
2090
+ this.#renameFor = r;
2091
+ this.#renameLabel = r.label ?? "";
2092
+ this.rerender();
2093
+ }) : null,
2094
+ this.#withDelete ? this.menuItem(
2095
+ "delete",
2096
+ this.t("backups.delete"),
2097
+ true,
2098
+ () => this.guarded({ type: "delete", fileId: r.fileId, name: r.name, active }, () => void this.deleteRow(r.fileId))
2099
+ ) : null
2100
+ ) : null
2101
+ );
2102
+ }
2103
+ passwordCard(r) {
2104
+ const submit = () => {
2105
+ if (this.#pw.length > 0) void this.open(r.fileId, this.#pw);
2106
+ };
2107
+ return h(
2108
+ "li",
2109
+ { part: "card", "data-password": r.fileId },
2110
+ h(
2111
+ "div",
2112
+ { style: "flex:1;min-width:0" },
2113
+ h("div", { part: "title" }, this.label(r)),
2114
+ h("div", { part: "sub" }, this.t("backups.pwHint")),
2115
+ h(
2116
+ "div",
2117
+ { part: "row", style: "margin-top:0.45rem" },
2118
+ h(
2119
+ "span",
2120
+ { part: "field", style: "flex:1;min-width:9rem" },
2121
+ h("input", {
2122
+ part: "input",
2123
+ type: this.#pwShown ? "text" : "password",
2124
+ autocomplete: "current-password",
2125
+ "data-keep": "backup-pw",
2126
+ disabled: this.#busy,
2127
+ oninput: (e) => this.#pw = e.target.value,
2128
+ onkeydown: (e) => {
2129
+ if (e.key === "Enter") submit();
2130
+ }
2131
+ }),
2132
+ this.eye()
2133
+ ),
2134
+ h("button", { part: "button button-primary", "data-action": "pw-open", disabled: this.#busy, onclick: submit }, this.t("backups.open")),
2135
+ h("button", { part: "button", "data-action": "pw-cancel", disabled: this.#busy, onclick: () => this.closePw() }, this.t("backups.cancel"))
2136
+ )
2137
+ )
2138
+ );
2139
+ }
2140
+ renameCard(r) {
2141
+ const taken = this.nameTaken(this.#renameLabel, r.fileId);
2142
+ const clean = this.#renameLabel.trim();
2143
+ return h(
2144
+ "li",
2145
+ { part: "card", "data-rename": r.fileId },
2146
+ h(
2147
+ "div",
2148
+ { style: "flex:1;min-width:0" },
2149
+ h("div", { part: "title" }, this.t("backups.renameTitle")),
2150
+ h("input", {
2151
+ part: "input",
2152
+ style: "margin-top:0.45rem",
2153
+ "data-keep": "rename-label",
2154
+ maxlength: "40",
2155
+ disabled: this.#busy,
2156
+ value: this.#renameLabel,
2157
+ oninput: (e) => {
2158
+ this.#renameLabel = e.target.value;
2159
+ this.rerender();
2160
+ }
2161
+ }),
2162
+ taken ? h("div", { part: "error-note" }, this.t("backups.taken")) : clean ? h("div", { part: "sub" }, this.ts("backups.willBe", { file: this.#manager?.fileNameFor(clean) ?? "" })) : null,
2163
+ h(
2164
+ "div",
2165
+ { part: "row", style: "margin-top:0.45rem" },
2166
+ h("button", { part: "button button-primary", "data-action": "rename-save", disabled: this.#busy || !clean || taken, onclick: () => void this.rename(r) }, this.t("backups.rename")),
2167
+ h(
2168
+ "button",
2169
+ { part: "button", "data-action": "rename-cancel", disabled: this.#busy, onclick: () => {
2170
+ this.#renameFor = null;
2171
+ this.rerender();
2172
+ } },
2173
+ this.t("backups.cancel")
2174
+ )
2175
+ )
2176
+ )
2177
+ );
2178
+ }
2179
+ createCard() {
2180
+ const taken = this.nameTaken(this.#createLabel, null);
2181
+ const clean = this.#createLabel.trim();
2182
+ return h(
2183
+ "div",
2184
+ { part: "card", "data-create": "" },
2185
+ h(
2186
+ "div",
2187
+ { style: "flex:1;min-width:0" },
2188
+ h("div", { part: "title" }, this.t("backups.newTitle")),
2189
+ h("div", { part: "sub" }, this.t("backups.startsBlank")),
2190
+ h("input", {
2191
+ part: "input",
2192
+ style: "margin-top:0.45rem",
2193
+ placeholder: this.t("backups.namePh"),
2194
+ "data-keep": "create-label",
2195
+ maxlength: "40",
2196
+ disabled: this.#busy,
2197
+ oninput: (e) => {
2198
+ this.#createLabel = e.target.value;
2199
+ this.rerender();
2200
+ }
2201
+ }),
2202
+ taken ? h("div", { part: "error-note" }, this.t("backups.taken")) : clean ? h("div", { part: "sub" }, this.ts("backups.willBe", { file: this.#manager?.fileNameFor(clean) ?? "" })) : null,
2203
+ h(
2204
+ "div",
2205
+ { part: "row", style: "margin-top:0.45rem" },
2206
+ h("button", { part: "button button-primary", "data-action": "create", disabled: this.#busy || !clean || taken, onclick: () => void this.create() }, this.t("backups.create")),
2207
+ h(
2208
+ "button",
2209
+ { part: "button", "data-action": "create-cancel", disabled: this.#busy, onclick: () => {
2210
+ this.#createOpen = false;
2211
+ this.rerender();
2212
+ } },
2213
+ this.t("backups.cancel")
2214
+ )
2215
+ )
2216
+ )
2217
+ );
2218
+ }
2219
+ // --- Backup copy (replica) -------------------------------------------------
2220
+ /** The transient setup UI under the active card: the destination choice or a
2221
+ * config form. The ATTACHED copy is not here - it rides inside the active
2222
+ * card (replicaAttachedLine). Idle renders nothing - the entry point lives
2223
+ * in the active card's menu. */
2224
+ replicaSetup() {
2225
+ const snap = this.#replicaSnap;
2226
+ if (!this.#replicaFlow || !snap || snap.replica) return [];
2227
+ if (snap.step === "choose") return [this.replicaPicker(snap)];
2228
+ if (snap.step === "form-webdav") return [this.replicaWebdavForm(snap)];
2229
+ if (snap.step === "form-s3") return [this.replicaS3Form(snap)];
2230
+ return [];
2231
+ }
2232
+ /** The attached copy is not a backup of its own: it renders as ONE line
2233
+ * INSIDE the active backup's card, under its pills (label, freshness, an
2234
+ * inline "remove") - never a card or a block of its own. Null when no copy
2235
+ * is attached. */
2236
+ replicaAttachedLine() {
2237
+ const snap = this.#replicaSnap;
2238
+ if (!this.#replicaFlow || !snap || !snap.replica) return null;
2239
+ const rep = snap.replica;
2240
+ const label = rep.label;
2241
+ const status = rep.lastError ? h("span", { part: "sub replica-error" }, this.t("backups.replica.error")) : rep.lastPublishAt ? h(
2242
+ "span",
2243
+ { part: "sub replica-ok" },
2244
+ this.ts("backups.replica.uptodate", {
2245
+ when: new Date(rep.lastPublishAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
2246
+ })
2247
+ ) : h("span", { part: "sub" }, this.t("backups.replica.pending"));
2248
+ return h(
2249
+ "div",
2250
+ {
2251
+ part: "sub replica-line",
2252
+ "data-replica": "",
2253
+ style: "display:flex;flex-wrap:wrap;align-items:baseline;gap:0.1rem 0.5rem;margin-top:0.35rem"
2254
+ },
2255
+ h("span", { part: "replica-label" }, this.ts("backups.replica.card", { label })),
2256
+ status,
2257
+ h(
2258
+ "button",
2259
+ {
2260
+ part: "link replica-remove",
2261
+ type: "button",
2262
+ "data-action": "replica-remove",
2263
+ onclick: () => this.guarded({ type: "replica-remove", label }, () => void this.#replicaFlow?.remove())
2264
+ },
2265
+ this.t("backups.replica.remove")
2266
+ )
2267
+ );
2268
+ }
2269
+ replicaPicker(snap) {
2270
+ return h(
2271
+ "li",
2272
+ { part: "card replica-picker", style: "display:block" },
2273
+ h("div", { part: "title" }, this.t("backups.replica.title")),
2274
+ h("div", { part: "sub", style: "margin:0.2rem 0 0.6rem" }, this.t("backups.replica.intro")),
2275
+ ...snap.kinds.map(
2276
+ (kind) => h(
2277
+ "button",
2278
+ {
2279
+ part: "button replica-dest",
2280
+ style: "display:block;width:100%;text-align:left;margin-bottom:0.4rem",
2281
+ "data-kind": kind,
2282
+ disabled: snap.busy,
2283
+ onclick: () => this.#replicaFlow?.pick(kind)
2284
+ },
2285
+ this.t(`backups.replica.dest.${kind}`)
2286
+ )
2287
+ ),
2288
+ snap.error ? h("div", { part: "error-note" }, this.t("error.generic")) : null,
2289
+ h(
2290
+ "button",
2291
+ { part: "button", "data-action": "replica-cancel", disabled: snap.busy, onclick: () => this.#replicaFlow?.cancel() },
2292
+ this.t("backups.cancel")
2293
+ )
2294
+ );
2295
+ }
2296
+ /** A config field: mutates state on input, no repaint (validated on submit). */
2297
+ replicaField(key, type, value, oninput) {
2298
+ return h("input", {
2299
+ part: "input",
2300
+ style: "display:block;width:100%;margin-bottom:0.4rem",
2301
+ type,
2302
+ placeholder: this.t(key),
2303
+ "aria-label": this.t(key),
2304
+ value,
2305
+ oninput: (e) => oninput(e.target.value)
2306
+ });
2307
+ }
2308
+ replicaWebdavForm(snap) {
2309
+ const c = this.#repWebdav;
2310
+ const submit = () => {
2311
+ if (!c.url.trim()) return;
2312
+ this.#replicaFlow?.submitWebdav({ url: c.url.trim(), username: c.username.trim(), password: c.password });
2313
+ };
2314
+ return h(
2315
+ "li",
2316
+ { part: "card replica-form", style: "display:block" },
2317
+ h("div", { part: "title", style: "margin-bottom:0.3rem" }, this.t("backups.replica.title")),
2318
+ h("div", { part: "sub", style: "margin-bottom:0.6rem" }, this.t("backups.replica.webdav.help")),
2319
+ this.replicaField("backups.replica.webdav.url", "url", c.url, (v) => c.url = v),
2320
+ this.replicaField("backups.replica.webdav.user", "text", c.username, (v) => c.username = v),
2321
+ this.replicaField("backups.replica.webdav.password", "password", c.password, (v) => c.password = v),
2322
+ snap.error ? h("div", { part: "error-note" }, this.t("error.generic")) : null,
2323
+ h(
2324
+ "div",
2325
+ { part: "row", style: "margin-top:0.45rem" },
2326
+ h("button", { part: "button button-primary", "data-action": "replica-webdav", disabled: snap.busy, onclick: submit }, this.t("backups.replica.save")),
2327
+ h("button", { part: "button", disabled: snap.busy, onclick: () => this.#replicaFlow?.cancel() }, this.t("backups.cancel"))
2328
+ )
2329
+ );
2330
+ }
2331
+ replicaS3Form(snap) {
2332
+ const c = this.#repS3;
2333
+ const submit = () => {
2334
+ const config = {
2335
+ endpoint: c.endpoint.trim(),
2336
+ region: c.region.trim(),
2337
+ bucket: c.bucket.trim(),
2338
+ key: c.key.trim(),
2339
+ accessKeyId: c.accessKeyId.trim(),
2340
+ secretAccessKey: c.secretAccessKey
2341
+ };
2342
+ if (!config.endpoint || !config.region || !config.bucket || !config.key) return;
2343
+ if (!config.accessKeyId || !config.secretAccessKey) return;
2344
+ this.#replicaFlow?.submitS3(config);
2345
+ };
2346
+ return h(
2347
+ "li",
2348
+ { part: "card replica-form", style: "display:block" },
2349
+ h("div", { part: "title", style: "margin-bottom:0.5rem" }, this.t("backups.replica.title")),
2350
+ this.replicaField("backups.replica.s3.endpoint", "url", c.endpoint, (v) => c.endpoint = v),
2351
+ this.replicaField("backups.replica.s3.region", "text", c.region, (v) => c.region = v),
2352
+ this.replicaField("backups.replica.s3.bucket", "text", c.bucket, (v) => c.bucket = v),
2353
+ this.replicaField("backups.replica.s3.key", "text", c.key, (v) => c.key = v),
2354
+ this.replicaField("backups.replica.s3.accessKeyId", "text", c.accessKeyId, (v) => c.accessKeyId = v),
2355
+ this.replicaField("backups.replica.s3.secret", "password", c.secretAccessKey, (v) => c.secretAccessKey = v),
2356
+ snap.error ? h("div", { part: "error-note" }, this.t("error.generic")) : null,
2357
+ h(
2358
+ "div",
2359
+ { part: "row", style: "margin-top:0.45rem" },
2360
+ h("button", { part: "button button-primary", "data-action": "replica-s3", disabled: snap.busy, onclick: submit }, this.t("backups.replica.save")),
2361
+ h("button", { part: "button", disabled: snap.busy, onclick: () => this.#replicaFlow?.cancel() }, this.t("backups.cancel"))
2362
+ )
2363
+ );
2364
+ }
2365
+ // --- Menu -----------------------------------------------------------------
2366
+ menuButton(key) {
2367
+ return h(
2368
+ "button",
2369
+ {
2370
+ part: "button menu-button",
2371
+ "aria-label": this.t("backups.menu"),
2372
+ "aria-haspopup": "menu",
2373
+ "aria-expanded": this.#menuFor === key ? "true" : "false",
2374
+ "data-action": "menu",
2375
+ "data-menu-key": key,
2376
+ disabled: this.#busy,
2377
+ onclick: () => {
2378
+ this.#menuFor = this.#menuFor === key ? null : key;
2379
+ this.rerender();
2380
+ this.root.querySelector('[part~="menu"]')?.focus();
2381
+ }
2382
+ },
2383
+ "\u22EF"
2384
+ );
2385
+ }
2386
+ /** The open overflow menu: a backdrop closes it on any outside click or
2387
+ * tap, Escape closes it and hands focus back to its trigger. The panel is
2388
+ * a dropdown over the trigger's corner on wide screens and a bottom sheet
2389
+ * on phones - both styled in the base sheet. */
2390
+ menu(...items) {
2391
+ const key = this.#menuFor;
2392
+ const close = () => {
2393
+ this.#menuFor = null;
2394
+ this.rerender();
2395
+ this.root.querySelector(`[data-menu-key="${key}"]`)?.focus();
2396
+ };
2397
+ return h(
2398
+ "div",
2399
+ { part: "menu-layer" },
2400
+ h("div", { part: "menu-backdrop", "data-action": "menu-close", onclick: close }),
2401
+ h(
2402
+ "div",
2403
+ {
2404
+ part: "menu",
2405
+ role: "menu",
2406
+ tabindex: "-1",
2407
+ onkeydown: (e) => {
2408
+ if (e.key === "Escape") close();
2409
+ }
2410
+ },
2411
+ ...items.filter((i) => i !== null)
2412
+ )
2413
+ );
2414
+ }
2415
+ menuItem(action, text, danger, run) {
2416
+ return h(
2417
+ "button",
2418
+ {
2419
+ part: danger ? "menu-item menu-item-danger" : "menu-item",
2420
+ type: "button",
2421
+ role: "menuitem",
2422
+ "data-action": action,
2423
+ onclick: () => {
2424
+ this.#menuFor = null;
2425
+ run();
2426
+ }
2427
+ },
2428
+ text
2429
+ );
2430
+ }
2431
+ eye() {
2432
+ const btn = h("button", {
2433
+ part: "eye",
2434
+ type: "button",
2435
+ "aria-label": this.t(this.#pwShown ? "backups.pwHide" : "backups.pwShow"),
2436
+ onclick: () => {
2437
+ this.#pwShown = !this.#pwShown;
2438
+ this.rerender();
2439
+ }
2440
+ });
2441
+ btn.innerHTML = this.#pwShown ? EYE_SHUT : EYE_OPEN;
2442
+ return btn;
2443
+ }
2444
+ // --- Gestures --------------------------------------------------------------
2445
+ nameTaken(label, exceptFileId) {
2446
+ const clean = label.trim().toLowerCase();
2447
+ if (!clean || this.#busy) return false;
2448
+ return (this.#rows ?? []).some(
2449
+ (r) => r.fileId !== exceptFileId && r.label !== null && r.label.trim().toLowerCase() === clean
2450
+ );
2451
+ }
2452
+ /** Run a destructive action through the host's veto (default: proceed).
2453
+ * A throwing hook reads as "no": destruction needs a clear yes. */
2454
+ guarded(action, run) {
2455
+ const gate = this.#confirmAction;
2456
+ if (!gate) {
2457
+ run();
2458
+ return;
2459
+ }
2460
+ void Promise.resolve().then(() => gate(action)).then((ok) => {
2461
+ if (ok) run();
2462
+ }).catch(() => void 0);
2463
+ }
2464
+ /** Open a row programmatically - the same gesture as tapping it, password
2465
+ * card included. Lets a host chain "open this backup, then act on it"
2466
+ * (its own sub-views) without re-implementing the passphrase step. */
2467
+ async open(fileId, passphrase) {
2468
+ const manager = this.#manager;
2469
+ if (!manager || this.#busy) return;
2470
+ this.#busy = true;
2471
+ this.#openBusy = fileId;
2472
+ this.rerender();
2473
+ try {
2474
+ const r = await manager.openBackup(fileId, passphrase ?? null);
2475
+ if (r === "encrypted") {
2476
+ this.#pwFor = fileId;
2477
+ if (passphrase) this.emit("selfstore-backups-wrong-password", { fileId });
2478
+ } else if (r === "ok") {
2479
+ this.closePw();
2480
+ this.emit("selfstore-backups-opened", { fileId });
2481
+ void this.loadRows();
2482
+ } else {
2483
+ this.emit("selfstore-backups-error", { code: manager.snapshot.lastError ?? "failed" });
2484
+ void this.loadRows();
2485
+ }
2486
+ } finally {
2487
+ this.#busy = false;
2488
+ this.#openBusy = null;
2489
+ this.rerender();
2490
+ }
2491
+ }
2492
+ async create() {
2493
+ const manager = this.#manager;
2494
+ const label = this.#createLabel.trim();
2495
+ if (!manager || !label || this.#busy) return;
2496
+ this.#busy = true;
2497
+ this.rerender();
2498
+ try {
2499
+ const r = await manager.createNamed(label);
2500
+ if (r === "ok") {
2501
+ this.#createOpen = false;
2502
+ this.#createLabel = "";
2503
+ this.emit("selfstore-backups-created", { label });
2504
+ void this.loadRows();
2505
+ } else {
2506
+ this.emit("selfstore-backups-error", { code: manager.snapshot.lastError ?? "failed" });
2507
+ }
2508
+ } finally {
2509
+ this.#busy = false;
2510
+ this.rerender();
2511
+ }
2512
+ }
2513
+ async rename(row) {
2514
+ const manager = this.#manager;
2515
+ const label = this.#renameLabel.trim();
2516
+ if (!manager || !label || this.#busy) return;
2517
+ this.#busy = true;
2518
+ this.rerender();
2519
+ try {
2520
+ const r = await manager.renameBackup(row.fileId, label);
2521
+ if (r === "ok") {
2522
+ this.#renameFor = null;
2523
+ this.emit("selfstore-backups-renamed", { fileId: row.fileId, label });
2524
+ void this.loadRows();
2525
+ } else {
2526
+ this.emit("selfstore-backups-error", { code: manager.snapshot.lastError ?? "failed" });
2527
+ }
2528
+ } finally {
2529
+ this.#busy = false;
2530
+ this.rerender();
2531
+ }
2532
+ }
2533
+ async deleteRow(fileId) {
2534
+ const manager = this.#manager;
2535
+ if (!manager || this.#busy) return;
2536
+ this.#busy = true;
2537
+ this.rerender();
2538
+ try {
2539
+ if (await manager.deleteBackup(fileId)) {
2540
+ this.#rows = (this.#rows ?? []).filter((r) => r.fileId !== fileId);
2541
+ this.emit("selfstore-backups-deleted", { fileId });
2542
+ } else {
2543
+ this.emit("selfstore-backups-error", { code: manager.snapshot.lastError ?? "failed" });
2544
+ }
2545
+ } finally {
2546
+ this.#busy = false;
2547
+ this.rerender();
2548
+ }
2549
+ }
2550
+ async forget(fileId) {
2551
+ const manager = this.#manager;
2552
+ if (!manager) return;
2553
+ await manager.forgetShared(fileId);
2554
+ this.emit("selfstore-backups-forgotten", { fileId });
2555
+ this.rerender();
2556
+ }
2557
+ closePw() {
2558
+ this.#pwFor = null;
2559
+ this.#pw = "";
2560
+ this.#pwShown = false;
2561
+ }
2562
+ };
2563
+ 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)";
2564
+
2565
+ // src/entries/widgets.ts
2566
+ function defineSelfstoreWidgets(prefix = "selfstore") {
2567
+ const define = (name, ctor) => {
2568
+ if (!customElements.get(name)) customElements.define(name, ctor);
2569
+ };
2570
+ define(`${prefix}-connect`, SelfstoreConnectElement);
2571
+ define(`${prefix}-share`, SelfstoreShareElement);
2572
+ define(`${prefix}-join`, SelfstoreJoinElement);
2573
+ define(`${prefix}-status`, SelfstoreStatusElement);
2574
+ define(`${prefix}-backups`, SelfstoreBackupsElement);
2575
+ }
2576
+
2577
+ export { FlowWidget, SelfstoreBackupsElement, SelfstoreConnectElement, SelfstoreJoinElement, SelfstoreShareElement, SelfstoreStatusElement, defineSelfstoreWidgets };
2578
+ //# sourceMappingURL=widgets.js.map
2579
+ //# sourceMappingURL=widgets.js.map