@meddleware/token-deployer-ui 0.0.7 → 0.0.9

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.
@@ -116,11 +116,16 @@ function onFile(e: Event): void {
116
116
  const f = (e.target as HTMLInputElement).files?.[0]
117
117
  if (!f) return
118
118
  const invalid = validateIconFile(f)
119
- if (invalid) { uploadError.value = invalid; return }
119
+ if (invalid) {
120
+ uploadError.value = invalid
121
+ return
122
+ }
120
123
  fileName.value = f.name
121
124
  fileSizeBytes.value = f.size
122
125
  const reader = new FileReader()
123
- reader.onload = () => { bytes = new Uint8Array(reader.result as ArrayBuffer) }
126
+ reader.onload = () => {
127
+ bytes = new Uint8Array(reader.result as ArrayBuffer)
128
+ }
124
129
  reader.readAsArrayBuffer(f)
125
130
  }
126
131
 
@@ -129,14 +134,19 @@ function onRelayChange(newHost: string): void {
129
134
  }
130
135
 
131
136
  async function upload(): Promise<void> {
132
- if (!bytes) { uploadError.value = 'Choose an image first.'; return }
133
- if (!account.value) { uploadError.value = 'Connect your wallet first.'; return }
137
+ if (!bytes) {
138
+ uploadError.value = 'Choose an image first.'
139
+ return
140
+ }
141
+ if (!account.value) {
142
+ uploadError.value = 'Connect your wallet first.'
143
+ return
144
+ }
134
145
  uploading.value = true
135
146
  uploadError.value = null
136
147
  try {
137
- const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } = await import(
138
- '../lib/walrus.js'
139
- )
148
+ const { createWalrusClient, createBlobUploadFlow, walrusBlobUrl } =
149
+ await import('../lib/walrus.js')
140
150
  const executor = await buildDeployExecutor(props.network as Network)
141
151
  const suiClient = getReadClient(props.network as Network)
142
152
 
@@ -232,11 +242,21 @@ function onBrowseSelect(url: string): void {
232
242
  v-model="tab"
233
243
  :options="[
234
244
  { id: 'url', label: 'URL' },
235
- { id: 'upload', label: 'Upload to Walrus', disabled: !isWalrusNetwork, title: isWalrusNetwork ? undefined : 'Not available on localnet' },
236
- { id: 'browse', label: 'My Walrus blobs', disabled: !isWalrusNetwork, title: isWalrusNetwork ? undefined : 'Not available on localnet' },
245
+ {
246
+ id: 'upload',
247
+ label: 'Upload to Walrus',
248
+ disabled: !isWalrusNetwork,
249
+ title: isWalrusNetwork ? undefined : 'Not available on localnet',
250
+ },
251
+ {
252
+ id: 'browse',
253
+ label: 'My Walrus blobs',
254
+ disabled: !isWalrusNetwork,
255
+ title: isWalrusNetwork ? undefined : 'Not available on localnet',
256
+ },
237
257
  ]"
238
258
  aria-label="Icon source"
239
- style="margin-bottom: 0.75rem"
259
+ class="icon-source"
240
260
  />
241
261
 
242
262
  <!-- URL panel -->
@@ -263,7 +283,7 @@ function onBrowseSelect(url: string): void {
263
283
  accept="image/*"
264
284
  aria-label="Choose an image to upload to Walrus"
265
285
  aria-describedby="walrus-help"
266
- style="width: auto"
286
+ class="icon-file"
267
287
  @change="onFile"
268
288
  />
269
289
  <button type="button" :disabled="uploading || !connected || !fileName" @click="upload">
@@ -272,59 +292,70 @@ function onBrowseSelect(url: string): void {
272
292
  </button>
273
293
  </div>
274
294
 
275
- <label class="permanent-toggle">
276
- <input type="checkbox" v-model="permanent" />
277
- Permanent blob <span class="hint">(cannot be deleted)</span>
295
+ <!-- The help button sits beside the label, not inside it (a label may only contain its control). -->
296
+ <p class="permanent-toggle">
297
+ <label>
298
+ <input v-model="permanent" type="checkbox" />
299
+ Permanent blob <span class="hint">(cannot be deleted)</span>
300
+ </label>
278
301
  <UiFieldHint field-id="blob-permanent">
279
302
  A permanent blob cannot be deleted once registered, even within its storage period.
280
303
  Deletable blobs can be removed early by the owner to reclaim unused storage fees (WAL).
281
304
  Permanent is the safer default for a token icon that should remain available indefinitely.
282
305
  </UiFieldHint>
283
- </label>
284
-
285
- <fieldset v-if="fileName && availableRelays.length > 1" style="margin: 0.5rem 0">
286
- <legend>Upload relay</legend>
287
- <div style="display: flex; flex-direction: column; gap: 0.5rem">
288
- <label
289
- v-for="option in availableRelays"
290
- :key="option.host"
291
- style="display: flex; align-items: center; gap: 0.5rem"
292
- >
293
- <input
294
- type="radio"
295
- :value="option.host"
296
- :checked="selectedRelayHost === option.host"
297
- @change="(e) => onRelayChange((e.target as HTMLInputElement).value)"
298
- />
299
- <span>{{ option.label }}</span>
300
- </label>
301
- </div>
306
+ </p>
307
+
308
+ <fieldset v-if="fileName && availableRelays.length > 1" class="relay-choice">
309
+ <legend>
310
+ Upload relay
311
+ <UiFieldHint field-id="upload-relay">
312
+ Browsers can't write to Walrus directly, so the blob is sent through an upload relay.
313
+ The <b>public</b> relay is free; the <b>operator</b> relay is run by this app and may
314
+ charge a small SUI tip per upload. Both store the identical blob — pick whichever you
315
+ prefer.
316
+ </UiFieldHint>
317
+ </legend>
318
+ <label v-for="option in availableRelays" :key="option.host">
319
+ <input
320
+ type="radio"
321
+ name="upload-relay"
322
+ :value="option.host"
323
+ :checked="selectedRelayHost === option.host"
324
+ @change="(e) => onRelayChange((e.target as HTMLInputElement).value)"
325
+ />
326
+ <span>{{ option.label }}</span>
327
+ </label>
302
328
  </fieldset>
303
329
 
304
- <UiNotice v-if="fileName && estimatedCost" style="margin: 0.5rem 0">
330
+ <UiNotice v-if="fileName && estimatedCost" class="icon-notice">
305
331
  <strong>Estimated cost:</strong> {{ estimatedCost.label }}
306
332
  </UiNotice>
307
333
 
308
334
  <!-- NFT-gated relay: the operator relay is live but this wallet lacks the access NFT.
309
335
  The public relay is still available; purchasing unlocks the operator relay. -->
310
- <UiNotice v-if="fileName && purchaseAccessAvailable" style="margin: 0.5rem 0">
311
- <p style="margin: 0 0 0.5rem">
312
- The operator relay requires an access NFT. You can keep using the public relay, or
313
- purchase access to support this app’s relay.
314
- </p>
315
- <button type="button" :disabled="purchasing || !connected" @click="purchaseAccess">
336
+ <UiNotice v-if="fileName && purchaseAccessAvailable" class="icon-notice">
337
+ The operator relay requires an access NFT. You can keep using the public relay, or purchase
338
+ access to support this app’s relay.
339
+ <button
340
+ type="button"
341
+ class="icon-notice__cta"
342
+ :disabled="purchasing || !connected"
343
+ @click="purchaseAccess"
344
+ >
316
345
  {{ purchasing ? 'Purchasing…' : 'Purchase relay access' }}
317
346
  </button>
318
347
  </UiNotice>
319
348
 
320
- <p v-if="showFreeRelayTip" class="hint" style="margin: 0.5rem 0">
349
+ <p v-if="showFreeRelayTip" class="hint icon-notice">
321
350
  Tip: select the public relay above to avoid the relay fee.
322
351
  </p>
323
352
 
324
- <p id="walrus-help" class="hint" aria-live="polite">
353
+ <p id="walrus-help" class="upload-note" aria-live="polite">
325
354
  Max {{ maxSizeLabel }} · PNG/JPEG/WebP/SVG. Requires <strong>WAL</strong> (storage) and
326
- <strong>SUI</strong> (gas + relay fee) in your wallet; two approvals. Stored for
327
- ~{{ ICON_YEARS }} years. <span v-if="status">{{ status }}</span>
355
+ <strong>SUI</strong> (gas + relay fee) in your wallet; two approvals. Stored for ~{{
356
+ ICON_YEARS
357
+ }}
358
+ years. <span v-if="status">{{ status }}</span>
328
359
  </p>
329
360
  <UiNotice v-if="uploadError" type="error">{{ uploadError }}</UiNotice>
330
361
  </div>
@@ -337,6 +368,37 @@ function onBrowseSelect(url: string): void {
337
368
  </template>
338
369
 
339
370
  <style scoped>
371
+ .icon-source {
372
+ margin-bottom: 0.75rem;
373
+ }
374
+
375
+ .icon-file {
376
+ width: auto;
377
+ }
378
+
379
+ .relay-choice {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 0.5rem;
383
+ margin: 0.5rem 0;
384
+ }
385
+
386
+ .relay-choice label {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.5rem;
390
+ margin: 0;
391
+ }
392
+
393
+ .icon-notice {
394
+ margin: 0.5rem 0;
395
+ }
396
+
397
+ .icon-notice__cta {
398
+ display: block;
399
+ margin-top: 0.5rem;
400
+ }
401
+
340
402
  .icon-panel {
341
403
  padding-top: 0.5rem;
342
404
  }
@@ -345,12 +407,38 @@ function onBrowseSelect(url: string): void {
345
407
  display: flex;
346
408
  align-items: center;
347
409
  gap: 0.4rem;
348
- margin-top: 0.5rem;
410
+ /* Only span the content — otherwise the row stretches the full field width. */
411
+ width: fit-content;
412
+ margin: 0.5rem 0 0;
349
413
  font-size: 0.875rem;
414
+ }
415
+
416
+ .permanent-toggle label {
417
+ display: inline-flex;
418
+ align-items: center;
419
+ gap: 0.4rem;
420
+ margin: 0;
350
421
  cursor: pointer;
351
422
  }
352
423
 
353
424
  .permanent-toggle input[type='checkbox'] {
425
+ /* Override the global `input { width: 100% }` so the checkbox stays its
426
+ natural size instead of consuming most of the row. */
427
+ width: auto;
428
+ flex: none;
354
429
  margin: 0;
355
430
  }
431
+
432
+ /* Boxed, muted note that reads as section-level guidance — visually distinct
433
+ from the Permanent-blob toggle above it. */
434
+ .upload-note {
435
+ margin: 0.75rem 0 0;
436
+ padding: 0.5rem 0.7rem;
437
+ border: 1px solid var(--border);
438
+ border-radius: 8px;
439
+ background: var(--surface);
440
+ color: var(--muted);
441
+ font-size: 0.85rem;
442
+ line-height: 1.45;
443
+ }
356
444
  </style>
@@ -2,7 +2,7 @@
2
2
  import { computed, onMounted, ref } from 'vue'
3
3
  import { fetchLicenseList, fetchLicenseText, NO_LICENSE } from '../lib/licenses.js'
4
4
  import type { SpdxLicense } from '../lib/licenses.js'
5
- import { UiSelect, UiFieldHint, UiNotice } from '@meddleware/ui'
5
+ import { UiDialog, UiFieldHint, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
6
6
 
7
7
  const props = defineProps<{ modelValue: string }>()
8
8
  const emit = defineEmits<{
@@ -46,7 +46,7 @@ onMounted(async () => {
46
46
  })
47
47
 
48
48
  // License view modal
49
- const licenseDialogEl = ref<HTMLDialogElement>()
49
+ const licenseOpen = ref(false)
50
50
  const licenseText = ref<string | null>(null)
51
51
  const licenseLoading = ref(false)
52
52
  const licenseError = ref<string | null>(null)
@@ -63,86 +63,85 @@ async function openLicenseModal(): Promise<void> {
63
63
  licenseLoading.value = false
64
64
  }
65
65
  }
66
- licenseDialogEl.value?.showModal()
66
+ licenseOpen.value = true
67
67
  }
68
68
  </script>
69
69
 
70
70
  <template>
71
- <div>
72
- <label for="license-select">
73
- License
71
+ <UiFormField id="license-select" label="License">
72
+ <template #label-suffix>
74
73
  <UiFieldHint field-id="license-select">
75
74
  Applies only to the downloadable Move source package — embedded in the LICENSE file and
76
- source headers. Has <b>no effect on the on-chain token</b> or its transferability.
77
- Choose <b>CC0-1.0</b> (default) for public domain. Choose <b>None</b> to retain all rights.
75
+ source headers. Has <b>no effect on the on-chain token</b> or its transferability. Choose
76
+ <b>CC0-1.0</b> (default) for public domain. Choose <b>None</b> to retain all rights.
78
77
  </UiFieldHint>
79
- </label>
80
- <div class="license-row">
81
- <UiSelect
82
- id="license-select"
83
- v-model="selectModel"
84
- :disabled="loading || Boolean(error)"
85
- aria-describedby="license-load-status"
86
- >
87
- <option :value="NO_LICENSE.id" :title="NO_LICENSE.name">{{ NO_LICENSE.id }}</option>
88
- <optgroup v-if="popular.length" label="Popular">
89
- <option
90
- v-for="l in popular"
91
- :key="'p-' + l.id"
92
- :value="l.id"
93
- :title="`${l.id} — ${l.name}`"
94
- >{{ l.id }}</option>
95
- </optgroup>
96
- <optgroup v-if="all.length" label="All licenses">
97
- <option
98
- v-for="l in allExceptNone"
99
- :key="l.id"
100
- :value="l.id"
101
- :title="`${l.id} — ${l.name}`"
102
- >{{ l.id }}</option>
103
- </optgroup>
104
- </UiSelect>
105
- <button type="button" class="view-btn" :disabled="loading" @click="openLicenseModal">
106
- View
107
- </button>
108
- </div>
109
- <p id="license-load-status" class="hint" aria-live="polite">
110
- <span v-if="loading"><span class="spinner" aria-hidden="true"></span> Loading licenses from SPDX…</span>
111
- <span v-else-if="error" class="field-error">Couldn't load the license list: {{ error }}</span>
112
- </p>
113
- </div>
78
+ </template>
79
+ <template #default="{ attrs }">
80
+ <span class="license-row">
81
+ <UiSelect
82
+ v-bind="attrs"
83
+ v-model="selectModel"
84
+ :disabled="loading || Boolean(error)"
85
+ aria-describedby="license-load-status"
86
+ >
87
+ <option :value="NO_LICENSE.id" :title="NO_LICENSE.name">{{ NO_LICENSE.id }}</option>
88
+ <optgroup v-if="popular.length" label="Popular">
89
+ <option
90
+ v-for="l in popular"
91
+ :key="'p-' + l.id"
92
+ :value="l.id"
93
+ :title="`${l.id} — ${l.name}`"
94
+ >
95
+ {{ l.id }}
96
+ </option>
97
+ </optgroup>
98
+ <optgroup v-if="all.length" label="All licenses">
99
+ <option
100
+ v-for="l in allExceptNone"
101
+ :key="l.id"
102
+ :value="l.id"
103
+ :title="`${l.id} — ${l.name}`"
104
+ >
105
+ {{ l.id }}
106
+ </option>
107
+ </optgroup>
108
+ </UiSelect>
109
+ <button type="button" class="view-btn" :disabled="loading" @click="openLicenseModal">
110
+ View
111
+ </button>
112
+ </span>
113
+ </template>
114
+ </UiFormField>
115
+ <p id="license-load-status" class="hint" aria-live="polite">
116
+ <span v-if="loading"
117
+ ><span class="spinner" aria-hidden="true"></span> Loading licenses from SPDX…</span
118
+ >
119
+ <span v-else-if="error" class="field-error">Couldn't load the license list: {{ error }}</span>
120
+ </p>
114
121
 
115
- <dialog ref="licenseDialogEl" class="license-dialog" @click.self="licenseDialogEl?.close()">
116
- <div class="dialog-header">
117
- <h2>{{ currentLicenseName }}</h2>
118
- <button
119
- type="button"
120
- aria-label="Close"
121
- style="background: none; border: none; font-size: 1.5rem; padding: 0; cursor: pointer; color: var(--text)"
122
- @click="licenseDialogEl?.close()"
123
- >×</button>
124
- </div>
125
- <div v-if="licenseLoading" style="padding: 1rem 0">
122
+ <UiDialog v-model:open="licenseOpen" :title="currentLicenseName" width="min(680px, 92vw)">
123
+ <p v-if="licenseLoading" role="status">
126
124
  <span class="spinner" aria-hidden="true"></span> Loading…
127
- </div>
125
+ </p>
128
126
  <UiNotice v-else-if="licenseError" type="error">{{ licenseError }}</UiNotice>
129
- <template v-else-if="modelValue === NO_LICENSE.id">
130
- <p>No LICENSE file will be generated. All rights are reserved by the author.</p>
131
- </template>
127
+ <p v-else-if="modelValue === NO_LICENSE.id">
128
+ No LICENSE file will be generated. All rights are reserved by the author.
129
+ </p>
132
130
  <template v-else>
133
- <p style="margin-top: 0">
131
+ <p class="license-spdx">
134
132
  <a
135
133
  :href="`https://spdx.org/licenses/${modelValue}.html`"
136
134
  target="_blank"
137
135
  rel="noopener noreferrer"
138
- >View on SPDX ↗</a>
136
+ >View on SPDX ↗</a
137
+ >
139
138
  </p>
140
139
  <pre class="license-text">{{ licenseText }}</pre>
141
140
  </template>
142
- <div style="margin-top: 1rem; text-align: right">
143
- <button type="button" @click="licenseDialogEl?.close()">Close</button>
144
- </div>
145
- </dialog>
141
+ <template #actions="{ close }">
142
+ <button type="button" @click="close()">Close</button>
143
+ </template>
144
+ </UiDialog>
146
145
  </template>
147
146
 
148
147
  <style scoped>
@@ -161,8 +160,8 @@ async function openLicenseModal(): Promise<void> {
161
160
  white-space: nowrap;
162
161
  }
163
162
 
164
- .license-dialog {
165
- width: min(680px, 92vw);
163
+ .license-spdx {
164
+ margin: 0;
166
165
  }
167
166
 
168
167
  .license-text {
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ // Lists the coins the connected wallet has deployed (owned TreasuryCap<T> objects). Read-only,
3
+ // mirroring access-gate's "My Gates". Loads on mount + whenever the connected address changes.
4
+ import { computed, ref, watch } from 'vue'
5
+ import {
6
+ CopyableAddress,
7
+ ExplorerLink,
8
+ UiNotice,
9
+ suiExplorerUrl,
10
+ type SuiNetwork,
11
+ } from '@meddleware/ui'
12
+ import { listMyTokens, type DeployedToken } from '../lib/listMyTokens.js'
13
+ import type { Network } from '../lib/types.js'
14
+
15
+ const props = defineProps<{ owner: string | null; network: Network }>()
16
+
17
+ // SuiVision has no localnet explorer, so only build object links on public networks.
18
+ const explorerNetwork = computed<SuiNetwork | null>(() =>
19
+ props.network === 'localnet' ? null : props.network,
20
+ )
21
+ function objectHref(id: string): string | null {
22
+ return explorerNetwork.value ? suiExplorerUrl('object', id, explorerNetwork.value) : null
23
+ }
24
+
25
+ const tokens = ref<DeployedToken[]>([])
26
+ const loading = ref(false)
27
+ const error = ref<string | null>(null)
28
+
29
+ async function load(): Promise<void> {
30
+ if (!props.owner) {
31
+ tokens.value = []
32
+ return
33
+ }
34
+ loading.value = true
35
+ error.value = null
36
+ try {
37
+ tokens.value = await listMyTokens(props.owner, props.network)
38
+ } catch (e) {
39
+ error.value = e instanceof Error ? e.message : String(e)
40
+ tokens.value = []
41
+ } finally {
42
+ loading.value = false
43
+ }
44
+ }
45
+
46
+ watch(() => props.owner, load, { immediate: true })
47
+ defineExpose({ reload: load })
48
+ </script>
49
+
50
+ <template>
51
+ <div class="my-tokens">
52
+ <p v-if="loading" class="hint">Loading your deployed tokens…</p>
53
+ <UiNotice v-else-if="error" type="error">{{ error }}</UiNotice>
54
+ <p v-else-if="!tokens.length" class="hint">
55
+ No deployed tokens found for this wallet. Deploy one from the <strong>Deploy</strong> tab.
56
+ </p>
57
+ <ul v-else class="my-tokens__list">
58
+ <li v-for="t in tokens" :key="t.treasuryCapId" class="my-tokens__item">
59
+ <div class="my-tokens__label">{{ t.label }}</div>
60
+ <dl class="my-tokens__meta">
61
+ <dt>Coin type</dt>
62
+ <dd><CopyableAddress :address="t.coinType" label="Copy coin type" /></dd>
63
+ <dt>Package</dt>
64
+ <dd>
65
+ <CopyableAddress :address="t.packageId" label="Copy package id">
66
+ <ExplorerLink
67
+ v-if="objectHref(t.packageId)"
68
+ :href="objectHref(t.packageId)!"
69
+ :value="t.packageId"
70
+ />
71
+ </CopyableAddress>
72
+ </dd>
73
+ <dt>Treasury cap</dt>
74
+ <dd>
75
+ <CopyableAddress :address="t.treasuryCapId" label="Copy treasury cap id">
76
+ <ExplorerLink
77
+ v-if="objectHref(t.treasuryCapId)"
78
+ :href="objectHref(t.treasuryCapId)!"
79
+ :value="t.treasuryCapId"
80
+ />
81
+ </CopyableAddress>
82
+ </dd>
83
+ </dl>
84
+ </li>
85
+ </ul>
86
+ </div>
87
+ </template>
88
+
89
+ <style scoped>
90
+ .my-tokens__list {
91
+ list-style: none;
92
+ margin: 0;
93
+ padding: 0;
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 0.75rem;
97
+ }
98
+ .my-tokens__item {
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--radius);
101
+ padding: 0.75rem 0.9rem;
102
+ background: var(--surface);
103
+ }
104
+ .my-tokens__label {
105
+ font-weight: 600;
106
+ margin-bottom: 0.4rem;
107
+ }
108
+ .my-tokens__meta {
109
+ display: grid;
110
+ grid-template-columns: auto 1fr;
111
+ gap: 0.2rem 0.75rem;
112
+ margin: 0;
113
+ font-size: 0.8rem;
114
+ }
115
+ .my-tokens__meta dt {
116
+ color: var(--muted);
117
+ white-space: nowrap;
118
+ }
119
+ .my-tokens__meta dd {
120
+ margin: 0;
121
+ min-width: 0;
122
+ font-family: var(--mw-font-mono);
123
+ font-size: 0.72rem;
124
+ }
125
+ </style>
@@ -72,11 +72,9 @@ async function downloadPackage(): Promise<void> {
72
72
  </div>
73
73
  <UiNotice v-if="downloadError" type="error">{{ downloadError }}</UiNotice>
74
74
 
75
- <div style="margin-top: 1rem">
76
- <GithubPush :config="config" :result="result" />
77
- </div>
75
+ <GithubPush class="actions" :config="config" :result="result" />
78
76
 
79
- <p v-once class="hint" style="margin-top: 1rem">
77
+ <p v-once class="hint actions">
80
78
  The download is the full, buildable Move package (matching this on-chain deployment) — your
81
79
  TreasuryCap grants minting rights; keep it safe. This tool takes no custody of your token.
82
80
  </p>
@@ -61,7 +61,7 @@ const feeSui = Number(FEE_MIST) / 1e9
61
61
  token or caps.
62
62
  </p>
63
63
 
64
- <div class="row" style="margin-top: 1rem">
64
+ <div class="row actions">
65
65
  <button type="button" @click="$emit('back')">Back</button>
66
66
  <button type="button" class="primary" @click="$emit('confirm')">Confirm &amp; deploy</button>
67
67
  </div>