@meddleware/seal-ui 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/seal-ui",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "homepage": "https://docs.meddleware.co.uk/blockchain/sui/sealed-storage/",
5
5
  "description": "Standalone Vue 3 SPA for Sealed Storage — client-side Seal encryption + access-gated, decentralised Walrus storage on Sui.",
6
6
  "author": "MeddleWare <meddleware@proton.me>",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@meddleware/design-tokens": "^0.1.5",
44
44
  "@meddleware/seal-client": "^0.0.5",
45
- "@meddleware/ui": "^0.1.19",
45
+ "@meddleware/ui": "^0.1.21",
46
46
  "@meddleware/wallet-adapter": "^0.0.7",
47
47
  "@mysten/sui": "^2.31.0",
48
48
  "@mysten/wallet-standard": "^0.21.0",
@@ -8,7 +8,7 @@
8
8
  // global stylesheet (which restyles body / #app / bare inputs). The standalone app keeps those
9
9
  // globals via main.ts → styles.css.
10
10
  import { computed, onMounted, ref, watch } from 'vue'
11
- import { AppTabNav, type AppTab } from '@meddleware/ui'
11
+ import { AppTabNav, UiStepper, type AppTab, type StepperStep } from '@meddleware/ui'
12
12
  import { Transaction } from '@mysten/sui/transactions'
13
13
  import {
14
14
  buildPublishSealedContentTx,
@@ -37,6 +37,30 @@ const TABS: AppTab[] = [
37
37
  ]
38
38
  const tab = ref<string>('encrypt')
39
39
 
40
+ const ENC_STEPS: StepperStep[] = [
41
+ { id: 'policy', label: 'Policy' },
42
+ { id: 'details', label: 'Details' },
43
+ { id: 'confirm', label: 'Confirm' },
44
+ ]
45
+ const DEC_STEPS: StepperStep[] = [
46
+ { id: 'manifest', label: 'Manifest' },
47
+ { id: 'decrypt', label: 'Decrypt' },
48
+ ]
49
+ const UNLOCK_STEPS: StepperStep[] = [
50
+ { id: 'gate', label: 'Gate' },
51
+ { id: 'unlock', label: 'Unlock' },
52
+ ]
53
+
54
+ const encStep = ref(0)
55
+ const decStep = ref(0)
56
+ const unlockStep = ref(0)
57
+
58
+ watch(tab, () => {
59
+ encStep.value = 0
60
+ decStep.value = 0
61
+ unlockStep.value = 0
62
+ })
63
+
40
64
  // Deep-link from access-gate-ui: ?gate=<id> preselects the nft-gate policy + gate.
41
65
  // Force manual mode so the pre-set ID renders in the text input immediately, before suggestions load.
42
66
  onMounted(() => {
@@ -160,6 +184,7 @@ async function performEncrypt(): Promise<void> {
160
184
  label: encLabel.value || encFile.value.name,
161
185
  }
162
186
  status.value = 'Sealed. Keep the manifest below — you need it to decrypt.'
187
+ encStep.value = 2
163
188
  } catch (e) {
164
189
  errorMsg.value = e instanceof Error ? e.message : String(e)
165
190
  } finally {
@@ -285,6 +310,7 @@ async function performDiscover(): Promise<void> {
285
310
  status.value = discovered.value.length
286
311
  ? `Found ${discovered.value.length} item(s).`
287
312
  : 'No sealed content published for this gate.'
313
+ if (discovered.value.length) unlockStep.value = 1
288
314
  } catch (e) {
289
315
  errorMsg.value = e instanceof Error ? e.message : String(e)
290
316
  } finally {
@@ -349,157 +375,214 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
349
375
 
350
376
  <!-- Encrypt -->
351
377
  <section v-show="tab === 'encrypt'" class="card">
352
- <label class="field">
353
- <span>Policy</span>
354
- <select v-model="encType" :disabled="disabled">
355
- <option v-for="p in providers" :key="p.type" :value="p.type">
356
- {{ p.describe().label }}
357
- </option>
358
- </select>
359
- </label>
360
- <p v-if="encProvider" class="muted">{{ encProvider.describe().help }}</p>
361
-
362
- <template v-if="encProvider">
363
- <template v-for="f in encProvider.describe().encryptFields" :key="f.name">
364
- <!-- Gate ID field: show a picker when wallet-owned gates are available -->
365
- <label v-if="f.name === 'gateId'" class="field">
366
- <span>{{ f.label }}<template v-if="f.required"> *</template></span>
367
- <span v-if="encGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
368
- <template v-else-if="encGateSuggestions.length && !encGateManual">
369
- <select v-model="encValues[f.name]">
370
- <option value="">Select a gate…</option>
371
- <option v-for="s in encGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
372
- </select>
373
- <button class="link" style="margin-top:0.25rem" @click="encGateManual = true; encValues[f.name] = ''">Enter ID manually</button>
374
- </template>
375
- <template v-else>
376
- <input type="text" v-model="encValues[f.name]" :placeholder="f.help" />
377
- <button v-if="encGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="encGateManual = false; encValues[f.name] = ''">← Back to picker</button>
378
- </template>
379
- </label>
380
- <!-- All other fields: generic rendering -->
381
- <label v-else class="field" :class="{ checkbox: f.kind === 'boolean' }">
382
- <span>{{ f.label }}<template v-if="f.required"> *</template></span>
383
- <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="encValues[f.name]" />
384
- <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="encValues[f.name]" />
385
- <input v-else type="text" v-model="encValues[f.name]" :placeholder="f.help" />
386
- </label>
378
+ <UiStepper :steps="ENC_STEPS" v-model="encStep" />
379
+
380
+ <!-- Step 0: Policy -->
381
+ <template v-if="encStep === 0">
382
+ <label class="field">
383
+ <span>Policy</span>
384
+ <select v-model="encType" :disabled="disabled">
385
+ <option v-for="p in providers" :key="p.type" :value="p.type">
386
+ {{ p.describe().label }}
387
+ </option>
388
+ </select>
389
+ </label>
390
+ <p v-if="encProvider" class="muted">{{ encProvider.describe().help }}</p>
391
+ <div class="nav-row">
392
+ <button type="button" class="primary" :disabled="disabled || !encProvider" @click="encStep++">Next</button>
393
+ </div>
394
+ </template>
395
+
396
+ <!-- Step 1: Details (policy fields + file + label) -->
397
+ <template v-else-if="encStep === 1">
398
+ <template v-if="encProvider">
399
+ <template v-for="f in encProvider.describe().encryptFields" :key="f.name">
400
+ <!-- Gate ID field: show a picker when wallet-owned gates are available -->
401
+ <label v-if="f.name === 'gateId'" class="field">
402
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
403
+ <span v-if="encGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
404
+ <template v-else-if="encGateSuggestions.length && !encGateManual">
405
+ <select v-model="encValues[f.name]">
406
+ <option value="">Select a gate…</option>
407
+ <option v-for="s in encGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
408
+ </select>
409
+ <button class="link" style="margin-top:0.25rem" @click="encGateManual = true; encValues[f.name] = ''">Enter ID manually</button>
410
+ </template>
411
+ <template v-else>
412
+ <input type="text" v-model="encValues[f.name]" :placeholder="f.help" />
413
+ <button v-if="encGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="encGateManual = false; encValues[f.name] = ''">← Back to picker</button>
414
+ </template>
415
+ </label>
416
+ <!-- All other fields: generic rendering -->
417
+ <label v-else class="field" :class="{ checkbox: f.kind === 'boolean' }">
418
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
419
+ <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="encValues[f.name]" />
420
+ <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="encValues[f.name]" />
421
+ <input v-else type="text" v-model="encValues[f.name]" :placeholder="f.help" />
422
+ </label>
423
+ </template>
387
424
  </template>
425
+
426
+ <label class="field">
427
+ <span>Label (optional)</span>
428
+ <input type="text" v-model="encLabel" placeholder="A name to recognise this later" />
429
+ </label>
430
+
431
+ <label class="field">
432
+ <span>File</span>
433
+ <input type="file" @change="onEncFile" />
434
+ </label>
435
+
436
+ <div class="nav-row">
437
+ <button type="button" class="link" @click="encStep--">Back</button>
438
+ <button type="button" class="primary" :disabled="disabled || !encFile" @click="encStep++">Next</button>
439
+ </div>
388
440
  </template>
389
441
 
390
- <label class="field">
391
- <span>Label (optional)</span>
392
- <input type="text" v-model="encLabel" placeholder="A name to recognise this later" />
393
- </label>
394
-
395
- <label class="field">
396
- <span>File</span>
397
- <input type="file" @change="onEncFile" />
398
- </label>
399
-
400
- <button class="primary" :disabled="busy || disabled" @click="performEncrypt">
401
- {{ busy ? 'Working…' : 'Encrypt & store' }}
402
- </button>
403
-
404
- <div v-if="manifest" style="margin-top:1rem">
405
- <p class="muted">Manifest — save this; it's required to decrypt (it holds no secrets):</p>
406
- <pre class="manifest">{{ JSON.stringify(manifest, null, 2) }}</pre>
407
- <button class="link" @click="downloadManifest">Download manifest</button>
408
-
409
- <div v-if="manifest.policyType === 'nft-gate'" style="margin-top:0.75rem">
410
- <p class="muted">
411
- Optional: publish an on-chain pointer so this gate's pass-holders can discover and unlock
412
- this content in the Unlock tab (no need to share the manifest).
413
- </p>
414
- <button class="primary" :disabled="busy || !account" @click="performPublish">
415
- {{ busy ? 'Working…' : 'Publish discovery pointer' }}
416
- </button>
417
- <span v-if="!account" class="muted"> — connect a wallet first.</span>
418
- <p v-if="publishDigest" class="muted">Published in tx {{ publishDigest.slice(0, 10) }}…</p>
442
+ <!-- Step 2: Confirm + encrypt -->
443
+ <template v-else-if="encStep === 2">
444
+ <div v-if="!manifest" class="confirm-summary">
445
+ <p class="muted">Policy: <strong>{{ encProvider?.describe().label }}</strong></p>
446
+ <p class="muted">File: <strong>{{ encFile?.name ?? '—' }}</strong></p>
447
+ <p v-if="encLabel" class="muted">Label: <strong>{{ encLabel }}</strong></p>
448
+ </div>
449
+
450
+ <button v-if="!manifest" class="primary" :disabled="busy || disabled" @click="performEncrypt">
451
+ {{ busy ? 'Working…' : 'Encrypt & store' }}
452
+ </button>
453
+
454
+ <div v-if="manifest" style="margin-top:1rem">
455
+ <p class="muted">Manifest — save this; it's required to decrypt (it holds no secrets):</p>
456
+ <pre class="manifest">{{ JSON.stringify(manifest, null, 2) }}</pre>
457
+ <button class="link" @click="downloadManifest">Download manifest</button>
458
+
459
+ <div v-if="manifest.policyType === 'nft-gate'" style="margin-top:0.75rem">
460
+ <p class="muted">
461
+ Optional: publish an on-chain pointer so this gate's pass-holders can discover and unlock
462
+ this content in the Unlock tab (no need to share the manifest).
463
+ </p>
464
+ <button class="primary" :disabled="busy || !account" @click="performPublish">
465
+ {{ busy ? 'Working…' : 'Publish discovery pointer' }}
466
+ </button>
467
+ <span v-if="!account" class="muted"> — connect a wallet first.</span>
468
+ <p v-if="publishDigest" class="muted">Published in tx {{ publishDigest.slice(0, 10) }}…</p>
469
+ </div>
470
+ </div>
471
+
472
+ <div v-if="!manifest" class="nav-row">
473
+ <button type="button" class="link" @click="encStep--">Back</button>
419
474
  </div>
420
- </div>
475
+ </template>
421
476
  </section>
422
477
 
423
478
  <!-- Unlock -->
424
479
  <section v-show="tab === 'unlock'" class="card">
425
- <p class="muted">
426
- Discover content sealed to an access gate and decrypt it with a pass you hold.
427
- </p>
428
- <label class="field">
429
- <span>Access gate ID</span>
430
- <span v-if="unlockGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
431
- <template v-else-if="unlockGateSuggestions.length && !unlockGateManual">
432
- <select v-model="unlockGateId">
433
- <option value="">Select a gate…</option>
434
- <option v-for="s in unlockGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
435
- </select>
436
- <button class="link" style="margin-top:0.25rem" @click="unlockGateManual = true; unlockGateId = ''">Enter ID manually</button>
437
- </template>
438
- <template v-else>
439
- <input type="text" v-model="unlockGateId" placeholder="0x… gate object id" />
440
- <button v-if="unlockGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="unlockGateManual = false; unlockGateId = ''">← Back to picker</button>
441
- </template>
442
- </label>
443
- <button class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
444
- {{ busy ? 'Working…' : 'Find sealed content' }}
445
- </button>
480
+ <UiStepper :steps="UNLOCK_STEPS" v-model="unlockStep" />
446
481
 
447
- <div v-if="discovered.length" style="margin-top:1rem">
482
+ <!-- Step 0: Gate selector -->
483
+ <template v-if="unlockStep === 0">
484
+ <p class="muted">
485
+ Discover content sealed to an access gate and decrypt it with a pass you hold.
486
+ </p>
448
487
  <label class="field">
449
- <span>Your pass NFT ID *</span>
450
- <input type="text" v-model="unlockNftId" placeholder="0x… your AccessNFT for this gate" />
451
- </label>
452
- <label class="field checkbox">
453
- <input type="checkbox" v-model="unlockSoulbound" />
454
- <span>Pass is soulbound</span>
488
+ <span>Access gate ID</span>
489
+ <span v-if="unlockGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
490
+ <template v-else-if="unlockGateSuggestions.length && !unlockGateManual">
491
+ <select v-model="unlockGateId">
492
+ <option value="">Select a gate…</option>
493
+ <option v-for="s in unlockGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
494
+ </select>
495
+ <button class="link" style="margin-top:0.25rem" @click="unlockGateManual = true; unlockGateId = ''">Enter ID manually</button>
496
+ </template>
497
+ <template v-else>
498
+ <input type="text" v-model="unlockGateId" placeholder="0x… gate object id" />
499
+ <button v-if="unlockGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="unlockGateManual = false; unlockGateId = ''">← Back to picker</button>
500
+ </template>
455
501
  </label>
502
+ <button class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
503
+ {{ busy ? 'Working…' : 'Find sealed content' }}
504
+ </button>
505
+ </template>
456
506
 
457
- <div v-for="item in discovered" :key="item.contentId" class="card" style="margin:0.5rem 0">
458
- <strong>{{ item.label || '(untitled)' }}</strong>
459
- <p class="muted" style="word-break:break-all">blob {{ item.blobId }}</p>
460
- <button class="primary" :disabled="busy || !account || !unlockNftId" @click="performUnlock(item)">
461
- Unlock &amp; download
462
- </button>
463
- <span v-if="!account" class="muted"> — connect a wallet to decrypt.</span>
507
+ <!-- Step 1: Results + unlock -->
508
+ <template v-else-if="unlockStep === 1">
509
+ <div v-if="!discovered.length" class="muted">No sealed content found for this gate.</div>
510
+
511
+ <div v-if="discovered.length">
512
+ <label class="field">
513
+ <span>Your pass NFT ID *</span>
514
+ <input type="text" v-model="unlockNftId" placeholder="0x… your AccessNFT for this gate" />
515
+ </label>
516
+ <label class="field checkbox">
517
+ <input type="checkbox" v-model="unlockSoulbound" />
518
+ <span>Pass is soulbound</span>
519
+ </label>
520
+
521
+ <div v-for="item in discovered" :key="item.contentId" class="card" style="margin:0.5rem 0">
522
+ <strong>{{ item.label || '(untitled)' }}</strong>
523
+ <p class="muted" style="word-break:break-all">blob {{ item.blobId }}</p>
524
+ <button class="primary" :disabled="busy || !account || !unlockNftId" @click="performUnlock(item)">
525
+ Unlock &amp; download
526
+ </button>
527
+ <span v-if="!account" class="muted"> — connect a wallet to decrypt.</span>
528
+ </div>
529
+ </div>
530
+
531
+ <div class="nav-row">
532
+ <button type="button" class="link" @click="unlockStep--; discovered = []">Back</button>
464
533
  </div>
465
- </div>
534
+ </template>
466
535
  </section>
467
536
 
468
537
  <!-- Decrypt -->
469
538
  <section v-show="tab === 'decrypt'" class="card">
470
- <label class="field">
471
- <span>Manifest (paste JSON or upload)</span>
472
- <textarea v-model="decManifestText" placeholder='{ "policyType": "...", "id": "...", "blobId": "..." }'></textarea>
473
- </label>
474
- <input type="file" accept="application/json,.json" @change="onManifestFile" />
475
-
476
- <template v-if="decProvider">
477
- <p class="muted" style="margin-top:0.75rem">
478
- Policy: {{ decProvider.describe().label }} — {{ decProvider.describe().help }}
479
- </p>
480
- <label
481
- v-for="f in decProvider.describe().decryptFields"
482
- :key="f.name"
483
- class="field"
484
- :class="{ checkbox: f.kind === 'boolean' }"
485
- >
486
- <span>{{ f.label }}<template v-if="f.required"> *</template></span>
487
- <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="decValues[f.name]" />
488
- <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="decValues[f.name]" />
489
- <input
490
- v-else
491
- type="text"
492
- v-model="decValues[f.name]"
493
- :placeholder="decManifest?.params?.[f.name] != null ? String(decManifest.params[f.name]) : f.help"
494
- />
539
+ <UiStepper :steps="DEC_STEPS" v-model="decStep" />
540
+
541
+ <!-- Step 0: Manifest input -->
542
+ <template v-if="decStep === 0">
543
+ <label class="field">
544
+ <span>Manifest (paste JSON or upload)</span>
545
+ <textarea v-model="decManifestText" placeholder='{ "policyType": "...", "id": "...", "blobId": "..." }'></textarea>
495
546
  </label>
547
+ <input type="file" accept="application/json,.json" @change="onManifestFile" />
548
+ <p v-if="decManifestError" class="muted">{{ decManifestError }}</p>
549
+ <p v-else-if="decManifestText.trim() && !decManifest" class="muted">Unrecognised or invalid manifest.</p>
550
+ <div class="nav-row">
551
+ <button type="button" class="primary" :disabled="!decManifest" @click="decStep++">Next</button>
552
+ </div>
496
553
  </template>
497
- <p v-else-if="decManifestError" class="muted">{{ decManifestError }}</p>
498
- <p v-else-if="decManifestText.trim()" class="muted">Unrecognised or invalid manifest.</p>
499
554
 
500
- <button class="primary" :disabled="busy || !decProvider" @click="performDecrypt">
501
- {{ busy ? 'Working…' : 'Decrypt' }}
502
- </button>
555
+ <!-- Step 1: Policy fields + decrypt -->
556
+ <template v-else-if="decStep === 1">
557
+ <template v-if="decProvider">
558
+ <p class="muted" style="margin-top:0.75rem">
559
+ Policy: {{ decProvider.describe().label }} — {{ decProvider.describe().help }}
560
+ </p>
561
+ <label
562
+ v-for="f in decProvider.describe().decryptFields"
563
+ :key="f.name"
564
+ class="field"
565
+ :class="{ checkbox: f.kind === 'boolean' }"
566
+ >
567
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
568
+ <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="decValues[f.name]" />
569
+ <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="decValues[f.name]" />
570
+ <input
571
+ v-else
572
+ type="text"
573
+ v-model="decValues[f.name]"
574
+ :placeholder="decManifest?.params?.[f.name] != null ? String(decManifest.params[f.name]) : f.help"
575
+ />
576
+ </label>
577
+ </template>
578
+
579
+ <div class="nav-row">
580
+ <button type="button" class="link" @click="decStep--">Back</button>
581
+ <button class="primary" :disabled="busy || !decProvider" @click="performDecrypt">
582
+ {{ busy ? 'Working…' : 'Decrypt' }}
583
+ </button>
584
+ </div>
585
+ </template>
503
586
  </section>
504
587
 
505
588
  <p v-if="status" class="status muted">{{ status }}</p>
@@ -594,6 +677,22 @@ pre.manifest {
594
677
  font-size: 0.75rem;
595
678
  }
596
679
 
680
+ .confirm-summary {
681
+ padding: 0.6rem 0;
682
+ display: flex;
683
+ flex-direction: column;
684
+ gap: 0.2rem;
685
+ margin-bottom: 0.75rem;
686
+ }
687
+
688
+ .nav-row {
689
+ display: flex;
690
+ gap: 0.6rem;
691
+ justify-content: flex-end;
692
+ align-items: center;
693
+ margin-top: 0.75rem;
694
+ }
695
+
597
696
  .muted { color: var(--muted, #a89b96); font-size: 0.85rem; }
598
697
  .status { font-size: 0.85rem; margin-top: 0.5rem; }
599
698
  .disclaimer { margin-top: 2rem; }