@meddleware/seal-ui 0.0.19 → 0.0.20

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,9 +1,9 @@
1
1
  {
2
2
  "name": "@meddleware/seal-ui",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
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
- "author": "MeddleWare <meddleware@proton.me>",
6
+ "author": "MeddleWare <dev@meddleware.co.uk>",
7
7
  "license": "0BSD",
8
8
  "repository": {
9
9
  "type": "git",
@@ -34,31 +34,53 @@
34
34
  "preview": "vite preview",
35
35
  "type-check": "vue-tsc --noEmit",
36
36
  "test": "vitest run",
37
- "test:watch": "vitest"
37
+ "test:watch": "vitest",
38
+ "lint:css": "stylelint \"src/**/*.{css,vue}\"",
39
+ "lint:js": "eslint .",
40
+ "lint": "npm run lint:css && npm run lint:js && npm run lint:html",
41
+ "lint:html": "html-validate \"src/**/*.vue\""
38
42
  },
39
43
  "publishConfig": {
40
44
  "access": "public"
41
45
  },
42
46
  "dependencies": {
43
- "@meddleware/design-tokens": "^0.1.6",
44
- "@meddleware/seal-client": "^0.0.6",
45
- "@meddleware/ui": "^0.1.26",
46
- "@meddleware/wallet-adapter": "^0.0.8",
47
- "@mysten/sui": "^2.31.0",
47
+ "@meddleware/design-tokens": "^0.1.7",
48
+ "@meddleware/seal-client": "^0.0.7",
49
+ "@meddleware/ui": "^0.1.28",
50
+ "@meddleware/wallet-adapter": "^0.0.9",
51
+ "@mysten/sui": "^2.33.0",
48
52
  "@mysten/wallet-standard": "^0.21.0",
49
53
  "vue": "^3.5.43"
50
54
  },
51
55
  "devDependencies": {
52
56
  "@types/node": "~24.12.2",
53
57
  "@vitejs/plugin-vue": "^6.0.9",
58
+ "@vue/eslint-config-typescript": "^14.9.0",
54
59
  "@vue/test-utils": "^2.5.1",
60
+ "autoprefixer": "^10.4.20",
61
+ "eslint": "^10.11.0",
62
+ "eslint-plugin-vue": "^10.11.1",
63
+ "eslint-plugin-vuejs-accessibility": "^2.6.0",
64
+ "html-validate": "^11.16.1",
65
+ "html-validate-vue": "^9.3.1",
66
+ "jiti": "^2.7.0",
55
67
  "jsdom": "^30.1.0",
68
+ "postcss": "^8.4.49",
69
+ "postcss-html": "^2.0.0",
70
+ "stylelint": "^17.15.0",
71
+ "stylelint-config-standard": "^40.0.0",
56
72
  "typescript": "~6.0.0",
57
73
  "vite": "^8.3.0",
58
74
  "vitest": "~5.0.1",
75
+ "vue-eslint-parser": "^10.4.1",
59
76
  "vue-tsc": "~3.3.11"
60
77
  },
61
78
  "engines": {
62
79
  "node": "^22.18.0 || >=24.12.0"
63
- }
80
+ },
81
+ "browserslist": [
82
+ "> 0.5%",
83
+ "last 2 versions",
84
+ "not dead"
85
+ ]
64
86
  }
package/src/App.vue CHANGED
@@ -22,7 +22,9 @@ const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/
22
22
  </template>
23
23
  </AppHeader>
24
24
 
25
- <SealView />
25
+ <div class="app__content">
26
+ <SealView />
27
+ </div>
26
28
 
27
29
  <AppFooter :docs-url="DOCS_URL" :dev-url="DEV_URL" />
28
30
  </div>
@@ -35,6 +37,17 @@ const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/
35
37
  flex-direction: column;
36
38
  }
37
39
 
40
+ /* Centre the tool at the shared tool-content width when running standalone. The dashboard
41
+ supplies its own width container, so this lives in the shell, not SealView. */
42
+ .app__content {
43
+ flex: 1;
44
+ width: 100%;
45
+ max-width: var(--mw-tool-content-max);
46
+ margin: 0 auto;
47
+ box-sizing: border-box;
48
+ padding: 1.5rem 1.25rem 4rem;
49
+ }
50
+
38
51
  .badge {
39
52
  font-size: 0.75rem;
40
53
  padding: 0.2rem 0.5rem;
@@ -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, UiStepper, type AppTab, type StepperStep } from '@meddleware/ui'
11
+ import { AppTabNav, UiStepper, UiTabPanel, UiToolIntro, type AppTab, type StepperStep } from '@meddleware/ui'
12
12
  import { Transaction } from '@mysten/sui/transactions'
13
13
  import {
14
14
  buildPublishSealedContentTx,
@@ -111,6 +111,13 @@ async function fetchGateSuggestions(provider: SealPolicyProvider | null): Promis
111
111
  const encType = ref(providers[0]?.type ?? '')
112
112
  const encProvider = computed(() => providers.find((p) => p.type === encType.value) ?? null)
113
113
  const encValues = ref<Record<string, string | boolean>>({})
114
+
115
+ /** One input per policy field; its type follows the field kind (v-model handles each type). */
116
+ function fieldInputType(kind: string): string {
117
+ if (kind === 'datetime') return 'datetime-local'
118
+ if (kind === 'boolean') return 'checkbox'
119
+ return 'text'
120
+ }
114
121
  const encFile = ref<File | null>(null)
115
122
  const encLabel = ref('')
116
123
  const manifest = ref<SealedManifest | null>(null)
@@ -352,10 +359,10 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
352
359
  </script>
353
360
 
354
361
  <template>
355
- <div class="page">
356
- <p class="muted">Client-side encrypted, access-gated storage on Walrus + Sui.</p>
357
362
 
358
- <div v-if="!SEAL_CONFIGURED" class="notice notice--warn">
363
+ <UiToolIntro>Client-side encrypted, access-gated storage on Walrus + Sui.</UiToolIntro>
364
+
365
+ <p v-if="!SEAL_CONFIGURED" class="notice notice--warn">
359
366
  <template v-if="NETWORK === 'mainnet'">
360
367
  Seal is not configured for mainnet. Set
361
368
  <code>VITE_SEAL_PACKAGE_ID_MAINNET</code>,
@@ -366,15 +373,15 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
366
373
  This deployment has no Seal policy package or key-server committee configured
367
374
  (<code>VITE_SEAL_PACKAGE_ID_*</code> / <code>VITE_SEAL_SERVER_OBJECT_IDS_*</code>).
368
375
  </template>
369
- </div>
376
+ </p>
370
377
 
371
378
  <WalletGuard message="Connect a Sui wallet to encrypt and decrypt sealed content.">
372
- <AppTabNav :tabs="TABS" v-model="tab" style="margin: 1rem 0" />
379
+ <AppTabNav v-model="tab" :tabs="TABS" id-prefix="seal" aria-label="Sealed storage" class="seal-tabs" />
373
380
 
374
- <div v-if="errorMsg" class="notice notice--error">{{ errorMsg }}</div>
381
+ <p v-if="errorMsg" class="notice notice--error" role="alert">{{ errorMsg }}</p>
375
382
 
376
383
  <!-- Encrypt -->
377
- <section v-show="tab === 'encrypt'" class="card">
384
+ <UiTabPanel v-show="tab === 'encrypt'" id-prefix="seal" tab="encrypt" class="card">
378
385
  <UiStepper :steps="ENC_STEPS" v-model="encStep" />
379
386
 
380
387
  <!-- Step 0: Policy -->
@@ -398,27 +405,33 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
398
405
  <template v-if="encProvider">
399
406
  <template v-for="f in encProvider.describe().encryptFields" :key="f.name">
400
407
  <!-- 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">
408
+ <!-- The picker/manual toggle is a sibling of the label: a label may only contain its own control. -->
409
+ <template v-if="f.name === 'gateId'">
410
+ <label v-if="!encGateLoading && encGateSuggestions.length && !encGateManual" class="field">
411
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
405
412
  <select v-model="encValues[f.name]">
406
413
  <option value="">Select a gate…</option>
407
414
  <option v-for="s in encGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
408
415
  </select>
409
- <button class="link" style="margin-top:0.25rem" @click="encGateManual = true; encValues[f.name] = ''">Enter ID manually</button>
416
+ </label>
417
+ <label v-else class="field">
418
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
419
+ <span v-if="encGateLoading" class="muted muted--sm">Loading your gates…</span>
420
+ <input v-else v-model="encValues[f.name]" type="text" :placeholder="f.help" />
421
+ </label>
422
+ <template v-if="!encGateLoading && encGateSuggestions.length">
423
+ <button v-if="!encGateManual" type="button" class="link link--spaced" @click="encGateManual = true; encValues[f.name] = ''">Enter ID manually</button>
424
+ <button v-else type="button" class="link link--spaced" @click="encGateManual = false; encValues[f.name] = ''">← Back to picker</button>
410
425
  </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>
426
+ </template>
416
427
  <!-- All other fields: generic rendering -->
417
428
  <label v-else class="field" :class="{ checkbox: f.kind === 'boolean' }">
418
429
  <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" />
430
+ <input
431
+ v-model="encValues[f.name]"
432
+ :type="fieldInputType(f.kind)"
433
+ :placeholder="fieldInputType(f.kind) === 'text' ? f.help : undefined"
434
+ />
422
435
  </label>
423
436
  </template>
424
437
  </template>
@@ -447,36 +460,38 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
447
460
  <p v-if="encLabel" class="muted">Label: <strong>{{ encLabel }}</strong></p>
448
461
  </div>
449
462
 
450
- <button v-if="!manifest" class="primary" :disabled="busy || disabled" @click="performEncrypt">
463
+ <button type="button" v-if="!manifest" class="primary" :disabled="busy || disabled" @click="performEncrypt">
451
464
  {{ busy ? 'Working…' : 'Encrypt & store' }}
452
465
  </button>
453
466
 
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>
467
+ <template v-if="manifest">
468
+ <figure class="enc-result">
469
+ <figcaption class="muted">Manifest — save this; it's required to decrypt (it holds no secrets):</figcaption>
470
+ <pre class="manifest">{{ JSON.stringify(manifest, null, 2) }}</pre>
471
+ </figure>
472
+ <button type="button" class="link" @click="downloadManifest">Download manifest</button>
458
473
 
459
- <div v-if="manifest.policyType === 'nft-gate'" style="margin-top:0.75rem">
474
+ <section v-if="manifest.policyType === 'nft-gate'" class="enc-publish">
460
475
  <p class="muted">
461
476
  Optional: publish an on-chain pointer so this gate's pass-holders can discover and unlock
462
477
  this content in the Unlock tab (no need to share the manifest).
463
478
  </p>
464
- <button class="primary" :disabled="busy || !account" @click="performPublish">
479
+ <button type="button" class="primary" :disabled="busy || !account" @click="performPublish">
465
480
  {{ busy ? 'Working…' : 'Publish discovery pointer' }}
466
481
  </button>
467
482
  <span v-if="!account" class="muted"> — connect a wallet first.</span>
468
483
  <p v-if="publishDigest" class="muted">Published in tx {{ publishDigest.slice(0, 10) }}…</p>
469
- </div>
470
- </div>
484
+ </section>
485
+ </template>
471
486
 
472
487
  <div v-if="!manifest" class="nav-row">
473
488
  <button type="button" class="link" @click="encStep--">Back</button>
474
489
  </div>
475
490
  </template>
476
- </section>
491
+ </UiTabPanel>
477
492
 
478
493
  <!-- Unlock -->
479
- <section v-show="tab === 'unlock'" class="card">
494
+ <UiTabPanel v-show="tab === 'unlock'" id-prefix="seal" tab="unlock" class="card">
480
495
  <UiStepper :steps="UNLOCK_STEPS" v-model="unlockStep" />
481
496
 
482
497
  <!-- Step 0: Gate selector -->
@@ -484,22 +499,23 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
484
499
  <p class="muted">
485
500
  Discover content sealed to an access gate and decrypt it with a pass you hold.
486
501
  </p>
487
- <label class="field">
502
+ <label v-if="!unlockGateLoading && unlockGateSuggestions.length && !unlockGateManual" class="field">
488
503
  <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>
504
+ <select v-model="unlockGateId">
505
+ <option value="">Select a gate…</option>
506
+ <option v-for="s in unlockGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
507
+ </select>
508
+ </label>
509
+ <label v-else class="field">
510
+ <span>Access gate ID</span>
511
+ <span v-if="unlockGateLoading" class="muted muted--sm">Loading your gates…</span>
512
+ <input v-else v-model="unlockGateId" type="text" placeholder="0x… gate object id" />
501
513
  </label>
502
- <button class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
514
+ <template v-if="!unlockGateLoading && unlockGateSuggestions.length">
515
+ <button v-if="!unlockGateManual" type="button" class="link link--spaced" @click="unlockGateManual = true; unlockGateId = ''">Enter ID manually</button>
516
+ <button v-else type="button" class="link link--spaced" @click="unlockGateManual = false; unlockGateId = ''">← Back to picker</button>
517
+ </template>
518
+ <button type="button" class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
503
519
  {{ busy ? 'Working…' : 'Find sealed content' }}
504
520
  </button>
505
521
  </template>
@@ -518,24 +534,24 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
518
534
  <span>Pass is soulbound</span>
519
535
  </label>
520
536
 
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)">
537
+ <article v-for="item in discovered" :key="item.contentId" class="card discovered">
538
+ <h3 class="discovered__title">{{ item.label || '(untitled)' }}</h3>
539
+ <p class="muted discovered__blob">blob {{ item.blobId }}</p>
540
+ <button type="button" class="primary" :disabled="busy || !account || !unlockNftId" @click="performUnlock(item)">
525
541
  Unlock &amp; download
526
542
  </button>
527
543
  <span v-if="!account" class="muted"> — connect a wallet to decrypt.</span>
528
- </div>
544
+ </article>
529
545
  </div>
530
546
 
531
547
  <div class="nav-row">
532
548
  <button type="button" class="link" @click="unlockStep--; discovered = []">Back</button>
533
549
  </div>
534
550
  </template>
535
- </section>
551
+ </UiTabPanel>
536
552
 
537
553
  <!-- Decrypt -->
538
- <section v-show="tab === 'decrypt'" class="card">
554
+ <UiTabPanel v-show="tab === 'decrypt'" id-prefix="seal" tab="decrypt" class="card">
539
555
  <UiStepper :steps="DEC_STEPS" v-model="decStep" />
540
556
 
541
557
  <!-- Step 0: Manifest input -->
@@ -544,7 +560,10 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
544
560
  <span>Manifest (paste JSON or upload)</span>
545
561
  <textarea v-model="decManifestText" placeholder='{ "policyType": "...", "id": "...", "blobId": "..." }'></textarea>
546
562
  </label>
547
- <input type="file" accept="application/json,.json" @change="onManifestFile" />
563
+ <label class="field">
564
+ <span>Or upload a manifest file</span>
565
+ <input type="file" accept="application/json,.json" @change="onManifestFile" />
566
+ </label>
548
567
  <p v-if="decManifestError" class="muted">{{ decManifestError }}</p>
549
568
  <p v-else-if="decManifestText.trim() && !decManifest" class="muted">Unrecognised or invalid manifest.</p>
550
569
  <div class="nav-row">
@@ -555,7 +574,7 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
555
574
  <!-- Step 1: Policy fields + decrypt -->
556
575
  <template v-else-if="decStep === 1">
557
576
  <template v-if="decProvider">
558
- <p class="muted" style="margin-top:0.75rem">
577
+ <p class="muted muted--spaced">
559
578
  Policy: {{ decProvider.describe().label }} — {{ decProvider.describe().help }}
560
579
  </p>
561
580
  <label
@@ -565,25 +584,23 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
565
584
  :class="{ checkbox: f.kind === 'boolean' }"
566
585
  >
567
586
  <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
587
  <input
571
- v-else
572
- type="text"
573
588
  v-model="decValues[f.name]"
574
- :placeholder="decManifest?.params?.[f.name] != null ? String(decManifest.params[f.name]) : f.help"
589
+ :type="fieldInputType(f.kind)"
590
+ :placeholder="fieldInputType(f.kind) !== 'text' ? undefined
591
+ : decManifest?.params?.[f.name] != null ? String(decManifest.params[f.name]) : f.help"
575
592
  />
576
593
  </label>
577
594
  </template>
578
595
 
579
596
  <div class="nav-row">
580
597
  <button type="button" class="link" @click="decStep--">Back</button>
581
- <button class="primary" :disabled="busy || !decProvider" @click="performDecrypt">
598
+ <button type="button" class="primary" :disabled="busy || !decProvider" @click="performDecrypt">
582
599
  {{ busy ? 'Working…' : 'Decrypt' }}
583
600
  </button>
584
601
  </div>
585
602
  </template>
586
- </section>
603
+ </UiTabPanel>
587
604
 
588
605
  <p v-if="status" class="status muted">{{ status }}</p>
589
606
 
@@ -592,16 +609,9 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
592
609
  servers are unreachable, decryption pauses — storage and retrieval are unaffected.
593
610
  </p>
594
611
  </WalletGuard>
595
- </div>
596
612
  </template>
597
613
 
598
614
  <style scoped>
599
- .page {
600
- max-width: 780px;
601
- margin: 0 auto;
602
- flex: 1;
603
- }
604
-
605
615
  .notice {
606
616
  border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
607
617
  border-radius: 10px;
@@ -694,6 +704,15 @@ pre.manifest {
694
704
  }
695
705
 
696
706
  .muted { color: var(--muted, #a89b96); font-size: 0.85rem; }
707
+ .muted--sm { font-size: 0.8rem; }
708
+ .muted--spaced { margin-top: 0.75rem; }
709
+ .link--spaced { margin-top: 0.25rem; }
710
+ .seal-tabs { margin: 1rem 0; }
711
+ .enc-result { margin: 1rem 0 0; }
712
+ .enc-publish { margin-top: 0.75rem; }
713
+ .discovered { margin: 0.5rem 0; }
714
+ .discovered__title { margin: 0; font-size: 1rem; }
715
+ .discovered__blob { word-break: break-all; }
697
716
  .status { font-size: 0.85rem; margin-top: 0.5rem; }
698
717
  .disclaimer { margin-top: 2rem; }
699
718
  </style>
package/src/styles.css CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  body {
14
14
  margin: 0;
15
- font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
15
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
16
16
  background: var(--bg, #14100f);
17
17
  color: var(--text, #f3ece9);
18
18
  }