@meddleware/token-deployer-ui 0.0.3 → 0.0.5

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/token-deployer-ui",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": { "default": "./src/index.ts" }
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@meddleware/design-tokens": "^0.1.5",
34
- "@meddleware/ui": "^0.1.22",
34
+ "@meddleware/ui": "^0.1.24",
35
35
  "@meddleware/wallet-adapter": "^0.0.7",
36
36
  "@meddleware/walrus-relay": "^0.1.16",
37
37
  "@mysten/bcs": "~2.1.0",
@@ -8,8 +8,7 @@ import LicensePicker from './LicensePicker.vue'
8
8
  import IconPicker from './IconPicker.vue'
9
9
  import { UiFormField, UiSelect, UiFieldHint } from '@meddleware/ui'
10
10
 
11
-
12
-
11
+ export type ConfigFormStep = 'identity' | 'token' | 'settings'
13
12
 
14
13
  const props = defineProps<{
15
14
  form: FormModel
@@ -17,8 +16,9 @@ const props = defineProps<{
17
16
  canProceed: boolean
18
17
  network: Network
19
18
  connected: boolean
19
+ formStep: ConfigFormStep
20
20
  }>()
21
- const emit = defineEmits<{ (e: 'submit'): void }>()
21
+ const emit = defineEmits<{ (e: 'next'): void; (e: 'back'): void }>()
22
22
 
23
23
  const structPreview = computed(() => {
24
24
  const m = props.form.moduleName.trim()
@@ -42,68 +42,75 @@ function onBlur(key: string): void {
42
42
  </script>
43
43
 
44
44
  <template>
45
- <form novalidate @submit.prevent="emit('submit')">
46
- <fieldset>
47
- <legend>Package identity <span class="permanent">(permanent)</span></legend>
48
- <div class="grid2">
49
- <UiFormField id="package" label="Package name" :error="visibleError('packageName')" v-slot="{ attrs }">
50
- <input v-bind="attrs" v-model="form.packageName" placeholder="my_token" autocomplete="off" @blur="onBlur('packageName')" />
51
- </UiFormField>
52
- <UiFormField id="module" label="Module name" :error="visibleError('moduleName')" v-slot="{ attrs }">
53
- <input v-bind="attrs" v-model="form.moduleName" placeholder="mytoken" autocomplete="off" @blur="onBlur('moduleName')" />
54
- </UiFormField>
55
- </div>
56
- <p class="hint" aria-live="polite">
57
- <template v-if="identifierError">
58
- Fix the package and module names to preview the coin type.
59
- </template>
60
- <template v-else>
61
- Coin type will be <span class="mono">{{ coinTypePreview }}</span
62
- >. The witness struct <span class="mono">{{ structPreview }}</span> is derived from
63
- the module name (a Sui requirement).
64
- </template>
65
- </p>
66
- </fieldset>
67
-
68
- <fieldset>
69
- <legend>Token details</legend>
70
-
71
- <div class="grid2">
72
- <UiFormField id="name" label="Token name" :error="visibleError('name')" v-slot="{ attrs }">
73
- <input v-bind="attrs" v-model="form.name" autocomplete="off" @blur="onBlur('name')" />
45
+ <form novalidate @submit.prevent="emit('next')">
46
+
47
+ <!-- Step 1: Identity -->
48
+ <template v-if="formStep === 'identity'">
49
+ <fieldset>
50
+ <legend>Package identity <span class="permanent">(permanent)</span></legend>
51
+ <div class="grid2">
52
+ <UiFormField id="package" label="Package name" :error="visibleError('packageName')" v-slot="{ attrs }">
53
+ <input v-bind="attrs" v-model="form.packageName" placeholder="my_token" autocomplete="off" @blur="onBlur('packageName')" />
54
+ </UiFormField>
55
+ <UiFormField id="module" label="Module name" :error="visibleError('moduleName')" v-slot="{ attrs }">
56
+ <input v-bind="attrs" v-model="form.moduleName" placeholder="mytoken" autocomplete="off" @blur="onBlur('moduleName')" />
57
+ </UiFormField>
58
+ </div>
59
+ <p class="hint" aria-live="polite">
60
+ <template v-if="identifierError">
61
+ Fix the package and module names to preview the coin type.
62
+ </template>
63
+ <template v-else>
64
+ Coin type will be <span class="mono">{{ coinTypePreview }}</span
65
+ >. The witness struct <span class="mono">{{ structPreview }}</span> is derived from
66
+ the module name (a Sui requirement).
67
+ </template>
68
+ </p>
69
+ </fieldset>
70
+ </template>
71
+
72
+ <!-- Step 2: Token -->
73
+ <template v-if="formStep === 'token'">
74
+ <fieldset>
75
+ <legend>Token details</legend>
76
+
77
+ <div class="grid2">
78
+ <UiFormField id="name" label="Token name" :error="visibleError('name')" v-slot="{ attrs }">
79
+ <input v-bind="attrs" v-model="form.name" autocomplete="off" @blur="onBlur('name')" />
80
+ </UiFormField>
81
+ <UiFormField id="symbol" label="Symbol" :error="visibleError('symbol')" v-slot="{ attrs }">
82
+ <input v-bind="attrs" v-model="form.symbol" autocomplete="off" @blur="onBlur('symbol')" />
83
+ </UiFormField>
84
+ </div>
85
+
86
+ <UiFormField id="description" label="Description" :error="visibleError('description')">
87
+ <template #label-suffix>&nbsp;<span class="hint">(optional)</span></template>
88
+ <template #default="{ attrs }">
89
+ <textarea v-bind="attrs" v-model="form.description" @blur="onBlur('description')" />
90
+ </template>
74
91
  </UiFormField>
75
- <UiFormField id="symbol" label="Symbol" :error="visibleError('symbol')" v-slot="{ attrs }">
76
- <input v-bind="attrs" v-model="form.symbol" autocomplete="off" @blur="onBlur('symbol')" />
92
+
93
+ <UiFormField id="decimals" label="Decimals" :error="visibleError('decimals')">
94
+ <template #label-suffix>&nbsp;<span class="permanent">(permanent)</span></template>
95
+ <template #default="{ attrs }">
96
+ <input v-bind="attrs" v-model="form.decimals" inputmode="numeric" @blur="onBlur('decimals')" />
97
+ </template>
77
98
  </UiFormField>
78
- </div>
79
-
80
- <UiFormField id="description" label="Description" :error="visibleError('description')">
81
- <template #label-suffix>&nbsp;<span class="hint">(optional)</span></template>
82
- <template #default="{ attrs }">
83
- <textarea v-bind="attrs" v-model="form.description" @blur="onBlur('description')" />
84
- </template>
85
- </UiFormField>
86
-
87
- <UiFormField id="decimals" label="Decimals" :error="visibleError('decimals')">
88
- <template #label-suffix>&nbsp;<span class="permanent">(permanent)</span></template>
89
- <template #default="{ attrs }">
90
- <input v-bind="attrs" v-model="form.decimals" inputmode="numeric" @blur="onBlur('decimals')" />
91
- </template>
92
- </UiFormField>
93
-
94
- <IconPicker
95
- v-model="form.iconUrl"
96
- :network="network"
97
- :connected="connected"
98
- :error="visibleError('iconUrl')"
99
- @blur="onBlur('iconUrl')"
100
- />
101
- </fieldset>
102
-
103
- <details>
104
- <summary>Advanced options</summary>
99
+
100
+ <IconPicker
101
+ v-model="form.iconUrl"
102
+ :network="network"
103
+ :connected="connected"
104
+ :error="visibleError('iconUrl')"
105
+ @blur="onBlur('iconUrl')"
106
+ />
107
+ </fieldset>
108
+ </template>
109
+
110
+ <!-- Step 3: Settings -->
111
+ <template v-if="formStep === 'settings'">
105
112
  <fieldset>
106
- <legend class="visually-hidden">Advanced options</legend>
113
+ <legend>Supply &amp; policies</legend>
107
114
 
108
115
  <UiFormField id="supply" label="Initial supply" :error="visibleError('initialSupply')">
109
116
  <template #label-suffix>
@@ -184,10 +191,13 @@ function onBlur(key: string): void {
184
191
 
185
192
  <LicensePicker v-model="form.license" @name="form.licenseName = $event" />
186
193
  </fieldset>
187
- </details>
194
+ </template>
188
195
 
189
196
  <div class="row" style="margin-top: 1rem">
190
- <button type="submit" class="primary" :disabled="!canProceed">Review &amp; deploy</button>
197
+ <button v-if="formStep !== 'identity'" type="button" @click="emit('back')">Back</button>
198
+ <button type="submit" class="primary" :disabled="!canProceed">
199
+ {{ formStep === 'settings' ? 'Review &amp; deploy' : 'Next' }}
200
+ </button>
191
201
  </div>
192
202
  </form>
193
203
  </template>
@@ -14,7 +14,7 @@ import {
14
14
  } from '../lib/accessGate.js'
15
15
  import type { WalrusNetwork } from '../lib/walrus.js'
16
16
  import type { Network } from '../lib/types.js'
17
- import { UiNotice, UiFieldHint } from '@meddleware/ui'
17
+ import { UiNotice, UiFieldHint, UiSegmentedControl } from '@meddleware/ui'
18
18
  import WalrusBlobBrowser from './WalrusBlobBrowser.vue'
19
19
  import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url'
20
20
 
@@ -76,7 +76,7 @@ async function purchaseAccess(): Promise<void> {
76
76
  }
77
77
  }
78
78
 
79
- const tab = ref<'url' | 'upload' | 'browse'>('url')
79
+ const tab = ref('url')
80
80
 
81
81
  // Upload state
82
82
  const uploading = ref(false)
@@ -228,26 +228,16 @@ function onBrowseSelect(url: string): void {
228
228
  <fieldset class="icon-picker">
229
229
  <legend>Icon <span class="hint">(optional)</span></legend>
230
230
 
231
- <div class="icon-tabs" role="group" aria-label="Icon source">
232
- <label :class="{ active: tab === 'url' }">
233
- <input type="radio" v-model="tab" value="url" />
234
- URL
235
- </label>
236
- <label
237
- :class="{ active: tab === 'upload', disabled: !isWalrusNetwork }"
238
- :title="isWalrusNetwork ? undefined : 'Not available on localnet'"
239
- >
240
- <input type="radio" v-model="tab" value="upload" :disabled="!isWalrusNetwork" />
241
- Upload to Walrus
242
- </label>
243
- <label
244
- :class="{ active: tab === 'browse', disabled: !isWalrusNetwork }"
245
- :title="isWalrusNetwork ? undefined : 'Not available on localnet'"
246
- >
247
- <input type="radio" v-model="tab" value="browse" :disabled="!isWalrusNetwork" />
248
- My Walrus blobs
249
- </label>
250
- </div>
231
+ <UiSegmentedControl
232
+ v-model="tab"
233
+ :options="[
234
+ { 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' },
237
+ ]"
238
+ aria-label="Icon source"
239
+ style="margin-bottom: 0.75rem"
240
+ />
251
241
 
252
242
  <!-- URL panel -->
253
243
  <div v-show="tab === 'url'" class="icon-panel">
@@ -347,49 +337,6 @@ function onBrowseSelect(url: string): void {
347
337
  </template>
348
338
 
349
339
  <style scoped>
350
- .icon-tabs {
351
- display: flex;
352
- border: 1px solid var(--border);
353
- border-radius: 6px;
354
- overflow: hidden;
355
- width: fit-content;
356
- margin-bottom: 0.75rem;
357
- }
358
-
359
- .icon-tabs label {
360
- display: flex;
361
- align-items: center;
362
- padding: 0.4rem 0.75rem;
363
- cursor: pointer;
364
- font-size: 0.875rem;
365
- background: transparent;
366
- border-right: 1px solid var(--border);
367
- transition: background 0.15s, color 0.15s;
368
- user-select: none;
369
- }
370
-
371
- .icon-tabs label:last-child {
372
- border-right: none;
373
- }
374
-
375
- .icon-tabs label.active {
376
- background: var(--accent);
377
- color: var(--accent-contrast);
378
- }
379
-
380
- .icon-tabs label.disabled {
381
- opacity: 0.5;
382
- cursor: not-allowed;
383
- pointer-events: none;
384
- }
385
-
386
- .icon-tabs input[type='radio'] {
387
- position: absolute;
388
- opacity: 0;
389
- width: 0;
390
- height: 0;
391
- }
392
-
393
340
  .icon-panel {
394
341
  padding-top: 0.5rem;
395
342
  }
@@ -1,14 +1,15 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, reactive, ref } from 'vue'
3
3
  import { useWallet, useNetwork, WalletGuard } from '@meddleware/wallet-adapter'
4
- import ConfigForm from './ConfigForm.vue'
4
+ import ConfigForm, { type ConfigFormStep } from './ConfigForm.vue'
5
5
  import ReviewPanel from './ReviewPanel.vue'
6
6
  import DeployProgress from './DeployProgress.vue'
7
7
  import ResultPanel from './ResultPanel.vue'
8
8
  import TermsDialog from './TermsDialog.vue'
9
- import { UiNotice } from '@meddleware/ui'
9
+ import { UiNotice, UiStepper, type StepperStep } from '@meddleware/ui'
10
10
  import { emptyForm, toTokenConfig } from '../lib/form.js'
11
- import { validateForm, isValid } from '../lib/validation.js'
11
+ import { validateForm } from '../lib/validation.js'
12
+ import type { FormErrors } from '../lib/validation.js'
12
13
  import { deployToken } from '../lib/deploy.js'
13
14
  import type { DeployStep } from '../lib/deploy.js'
14
15
  import { buildDeployExecutor } from '../lib/deployExecutor.js'
@@ -16,13 +17,23 @@ import { FEE_MIST, FEE_TREASURY, PUBLISH_GAS_BUDGET, isFeeConfigured } from '../
16
17
  import { extractErrorMessage } from '../lib/errors.js'
17
18
  import type { Network, PublishResult } from '../lib/types.js'
18
19
 
19
- type Step = 'form' | 'review' | 'deploying' | 'done'
20
+ type Step = 'identity' | 'token' | 'settings' | 'review' | 'deploying' | 'done'
21
+
22
+ const STEPS: StepperStep[] = [
23
+ { id: 'identity', label: 'Identity' },
24
+ { id: 'token', label: 'Token' },
25
+ { id: 'settings', label: 'Settings' },
26
+ { id: 'review', label: 'Review' },
27
+ { id: 'done', label: 'Done' },
28
+ ]
29
+
30
+ const CONFIG_STEPS: ConfigFormStep[] = ['identity', 'token', 'settings']
20
31
 
21
32
  const { account } = useWallet()
22
33
  const { network } = useNetwork()
23
34
 
24
35
  const form = reactive(emptyForm())
25
- const step = ref<Step>('form')
36
+ const step = ref<Step>('identity')
26
37
  const deployStep = ref<DeployStep | null>(null)
27
38
  const deployError = ref<string | null>(null)
28
39
  const result = ref<PublishResult | null>(null)
@@ -32,12 +43,65 @@ const showTerms = ref(false)
32
43
  const errors = computed(() => validateForm(form))
33
44
  const config = computed(() => toTokenConfig(form))
34
45
  const connected = computed(() => Boolean(account.value))
35
- const canProceed = computed(() => isValid(errors.value) && connected.value)
36
46
 
37
- function goReview(): void {
47
+ const IDENTITY_KEYS: (keyof FormErrors)[] = ['packageName', 'moduleName']
48
+ const TOKEN_KEYS: (keyof FormErrors)[] = ['name', 'symbol', 'description', 'iconUrl', 'decimals']
49
+ const SETTINGS_KEYS: (keyof FormErrors)[] = ['initialSupply', 'recipient']
50
+
51
+ function stepHasErrors(keys: (keyof FormErrors)[]): boolean {
52
+ return keys.some((k) => Boolean(errors.value[k]))
53
+ }
54
+
55
+ const canProceed = computed((): boolean => {
56
+ if (!connected.value) return false
57
+ switch (step.value) {
58
+ case 'identity': return !stepHasErrors(IDENTITY_KEYS)
59
+ case 'token': return !stepHasErrors(TOKEN_KEYS)
60
+ case 'settings': return !stepHasErrors(SETTINGS_KEYS)
61
+ default: return true
62
+ }
63
+ })
64
+
65
+ const formStep = computed((): ConfigFormStep => {
66
+ if (step.value === 'token') return 'token'
67
+ if (step.value === 'settings') return 'settings'
68
+ return 'identity'
69
+ })
70
+
71
+ const stepIndex = computed((): number => {
72
+ const map: Record<Step, number> = {
73
+ identity: 0,
74
+ token: 1,
75
+ settings: 2,
76
+ review: 3,
77
+ deploying: 3,
78
+ done: 4,
79
+ }
80
+ return map[step.value]
81
+ })
82
+
83
+ function onStepperBack(i: number): void {
84
+ const configStepAt = CONFIG_STEPS[i]
85
+ if (configStepAt && step.value === 'review') step.value = configStepAt
86
+ if (i < stepIndex.value && CONFIG_STEPS.includes(step.value as ConfigFormStep)) {
87
+ step.value = CONFIG_STEPS[i] ?? 'identity'
88
+ }
89
+ }
90
+
91
+ function goNext(): void {
38
92
  if (!canProceed.value) return
39
- if (!termsAccepted.value) { showTerms.value = true; return }
40
- step.value = 'review'
93
+ if (step.value === 'identity') { step.value = 'token'; return }
94
+ if (step.value === 'token') { step.value = 'settings'; return }
95
+ if (step.value === 'settings') {
96
+ if (!termsAccepted.value) { showTerms.value = true; return }
97
+ step.value = 'review'
98
+ }
99
+ }
100
+
101
+ function goBack(): void {
102
+ if (step.value === 'token') { step.value = 'identity'; return }
103
+ if (step.value === 'settings') { step.value = 'token'; return }
104
+ if (step.value === 'review') { step.value = 'settings'; return }
41
105
  }
42
106
 
43
107
  function onTermsAccept(): void {
@@ -80,26 +144,29 @@ async function confirmDeploy(): Promise<void> {
80
144
  function restart(): void {
81
145
  result.value = null
82
146
  deployError.value = null
83
- step.value = 'form'
147
+ step.value = 'identity'
84
148
  }
85
149
  </script>
86
150
 
87
151
  <template>
88
152
  <WalletGuard message="Connect a Sui wallet to deploy your token.">
153
+ <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
89
154
  <ConfigForm
90
- v-if="step === 'form'"
155
+ v-if="step === 'identity' || step === 'token' || step === 'settings'"
91
156
  :form="form"
92
157
  :errors="errors"
93
158
  :can-proceed="canProceed"
94
159
  :network="network"
95
160
  :connected="connected"
96
- @submit="goReview"
161
+ :form-step="formStep"
162
+ @next="goNext"
163
+ @back="goBack"
97
164
  />
98
165
  <ReviewPanel
99
166
  v-if="step === 'review'"
100
167
  :config="config"
101
168
  :network="network"
102
- @back="step = 'form'"
169
+ @back="goBack"
103
170
  @confirm="confirmDeploy"
104
171
  />
105
172
  <DeployProgress v-if="step === 'deploying'" :step="deployStep" />