@meddleware/token-deployer-ui 0.0.8 → 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,17 +292,20 @@ 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>
306
+ </p>
284
307
 
285
- <fieldset v-if="fileName && availableRelays.length > 1" style="margin: 0.5rem 0">
308
+ <fieldset v-if="fileName && availableRelays.length > 1" class="relay-choice">
286
309
  <legend>
287
310
  Upload relay
288
311
  <UiFieldHint field-id="upload-relay">
@@ -292,47 +315,47 @@ function onBrowseSelect(url: string): void {
292
315
  prefer.
293
316
  </UiFieldHint>
294
317
  </legend>
295
- <div style="display: flex; flex-direction: column; gap: 0.5rem">
296
- <label
297
- v-for="option in availableRelays"
298
- :key="option.host"
299
- style="display: flex; align-items: center; gap: 0.5rem"
300
- >
301
- <input
302
- type="radio"
303
- :value="option.host"
304
- :checked="selectedRelayHost === option.host"
305
- @change="(e) => onRelayChange((e.target as HTMLInputElement).value)"
306
- />
307
- <span>{{ option.label }}</span>
308
- </label>
309
- </div>
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>
310
328
  </fieldset>
311
329
 
312
- <UiNotice v-if="fileName && estimatedCost" style="margin: 0.5rem 0">
330
+ <UiNotice v-if="fileName && estimatedCost" class="icon-notice">
313
331
  <strong>Estimated cost:</strong> {{ estimatedCost.label }}
314
332
  </UiNotice>
315
333
 
316
334
  <!-- NFT-gated relay: the operator relay is live but this wallet lacks the access NFT.
317
335
  The public relay is still available; purchasing unlocks the operator relay. -->
318
- <UiNotice v-if="fileName && purchaseAccessAvailable" style="margin: 0.5rem 0">
319
- <p style="margin: 0 0 0.5rem">
320
- The operator relay requires an access NFT. You can keep using the public relay, or
321
- purchase access to support this app’s relay.
322
- </p>
323
- <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
+ >
324
345
  {{ purchasing ? 'Purchasing…' : 'Purchase relay access' }}
325
346
  </button>
326
347
  </UiNotice>
327
348
 
328
- <p v-if="showFreeRelayTip" class="hint" style="margin: 0.5rem 0">
349
+ <p v-if="showFreeRelayTip" class="hint icon-notice">
329
350
  Tip: select the public relay above to avoid the relay fee.
330
351
  </p>
331
352
 
332
353
  <p id="walrus-help" class="upload-note" aria-live="polite">
333
354
  Max {{ maxSizeLabel }} · PNG/JPEG/WebP/SVG. Requires <strong>WAL</strong> (storage) and
334
- <strong>SUI</strong> (gas + relay fee) in your wallet; two approvals. Stored for
335
- ~{{ 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>
336
359
  </p>
337
360
  <UiNotice v-if="uploadError" type="error">{{ uploadError }}</UiNotice>
338
361
  </div>
@@ -345,6 +368,37 @@ function onBrowseSelect(url: string): void {
345
368
  </template>
346
369
 
347
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
+
348
402
  .icon-panel {
349
403
  padding-top: 0.5rem;
350
404
  }
@@ -355,8 +409,15 @@ function onBrowseSelect(url: string): void {
355
409
  gap: 0.4rem;
356
410
  /* Only span the content — otherwise the row stretches the full field width. */
357
411
  width: fit-content;
358
- margin-top: 0.5rem;
412
+ margin: 0.5rem 0 0;
359
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;
360
421
  cursor: pointer;
361
422
  }
362
423
 
@@ -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>
@@ -1,26 +1,25 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch } from 'vue'
2
+ // Pre-deploy terms, in the shared UiDialog. Closing by any route (Cancel, Escape, backdrop,
3
+ // close button) counts as cancel; only the "continue" action accepts — UiDialog's return value
4
+ // tells them apart.
5
+ import { UiDialog } from '@meddleware/ui'
3
6
 
4
- const props = defineProps<{ open: boolean }>()
7
+ defineProps<{ open: boolean }>()
5
8
  const emit = defineEmits<{
6
9
  (e: 'accept'): void
7
10
  (e: 'cancel'): void
8
11
  }>()
9
12
 
10
- const el = ref<HTMLDialogElement>()
11
-
12
- watch(
13
- () => props.open,
14
- (v) => (v ? el.value?.showModal() : el.value?.close()),
15
- )
13
+ function onClose(returnValue: string): void {
14
+ if (returnValue === 'accept') emit('accept')
15
+ else emit('cancel')
16
+ }
16
17
  </script>
17
18
 
18
19
  <template>
19
- <dialog ref="el" @click.self="$emit('cancel')" @cancel.prevent="$emit('cancel')">
20
- <div class="dialog-header">
21
- <h2>Before you deploy</h2>
22
- </div>
23
- <ul>
20
+ <UiDialog :open="open" title="Before you deploy" @close="onClose">
21
+ <!-- A list: each item is a discrete, parallel term (announced with an item count). -->
22
+ <ul class="terms">
24
23
  <li>
25
24
  All operations — bytecode compilation, transaction construction, and signing — take place
26
25
  entirely <strong>client-side</strong>, meaning on your computer. No code runs on our
@@ -48,34 +47,36 @@ watch(
48
47
  arising from your use of this tool.
49
48
  </li>
50
49
  </ul>
51
- <p style="margin-bottom: 1.25rem; font-size: 0.9rem; color: var(--muted)">
50
+ <p class="terms__confirm">
52
51
  By continuing, you confirm that you have read and accept these terms.
53
52
  </p>
54
- <div class="terms-actions">
55
- <button type="button" class="primary" @click="$emit('accept')">I understand — continue</button>
56
- <button type="button" @click="$emit('cancel')">Cancel</button>
57
- </div>
58
- </dialog>
53
+ <template #actions="{ close }">
54
+ <button type="button" class="primary" @click="close('accept')">
55
+ I understand — continue
56
+ </button>
57
+ <button type="button" @click="close('cancel')">Cancel</button>
58
+ </template>
59
+ </UiDialog>
59
60
  </template>
60
61
 
61
62
  <style scoped>
62
- ul {
63
+ .terms {
63
64
  padding-left: 1.25rem;
64
- margin: 0 0 1rem;
65
+ margin: 0;
65
66
  line-height: 1.6;
66
67
  }
67
68
 
68
- ul li {
69
+ .terms li {
69
70
  margin-bottom: 0.6rem;
70
71
  }
71
72
 
72
- ul li:last-child {
73
+ .terms li:last-child {
73
74
  margin-bottom: 0;
74
75
  }
75
76
 
76
- .terms-actions {
77
- display: flex;
78
- gap: 0.75rem;
79
- justify-content: flex-end;
77
+ .terms__confirm {
78
+ margin: 0;
79
+ font-size: 0.9rem;
80
+ color: var(--muted);
80
81
  }
81
82
  </style>