@meddleware/token-deployer-ui 0.0.1

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.
Files changed (103) hide show
  1. package/.claude/move-language-reference/SKILL.md +445 -0
  2. package/.claude/skills/move-language-reference/SKILL.md +445 -0
  3. package/.claude/skills/sui-move-bootcamp/SKILL.md +128 -0
  4. package/.claude/sui-move-bootcamp/SKILL.md +128 -0
  5. package/.editorconfig +8 -0
  6. package/.env.example +75 -0
  7. package/.env.production +7 -0
  8. package/.gitattributes +1 -0
  9. package/.github/workflows/ci.yml +34 -0
  10. package/.github/workflows/token-deployer-e2e.yml +64 -0
  11. package/.oxfmtrc.json +5 -0
  12. package/.oxlintrc.json +10 -0
  13. package/.vscode/extensions.json +9 -0
  14. package/AGENTS.md +39 -0
  15. package/CLAUDE.md +159 -0
  16. package/LICENSE +121 -0
  17. package/README.md +234 -0
  18. package/env.d.ts +13 -0
  19. package/index.html +13 -0
  20. package/package.json +67 -0
  21. package/playwright.config.ts +30 -0
  22. package/public/CNAME +1 -0
  23. package/public/_headers +12 -0
  24. package/public/_redirects +1 -0
  25. package/public/favicon.ico +0 -0
  26. package/public/favicon.svg +5 -0
  27. package/public/og-image.png +0 -0
  28. package/public/robots.txt +3 -0
  29. package/public/sitemap.xml +8 -0
  30. package/scripts/e2e-browser.mjs +15 -0
  31. package/scripts/e2e-deploy.mjs +222 -0
  32. package/scripts/e2e-localnet.mjs +65 -0
  33. package/scripts/e2e-walrus-browser.mjs +115 -0
  34. package/scripts/gen-og-image.mjs +31 -0
  35. package/scripts/publish-localnet.mjs +55 -0
  36. package/scripts/regen-template.sh +45 -0
  37. package/scripts/sync-template-src.sh +36 -0
  38. package/scripts/verify-bytecode.mjs +49 -0
  39. package/src/App.vue +41 -0
  40. package/src/__tests__/App.spec.js +11 -0
  41. package/src/components/AppNotice.vue +15 -0
  42. package/src/components/ColorModeControl.vue +81 -0
  43. package/src/components/ConfigForm.vue +192 -0
  44. package/src/components/DeployProgress.vue +46 -0
  45. package/src/components/FieldHint.vue +79 -0
  46. package/src/components/FormField.vue +28 -0
  47. package/src/components/FormSelect.vue +43 -0
  48. package/src/components/GithubPush.vue +100 -0
  49. package/src/components/IconPicker.vue +410 -0
  50. package/src/components/LicensePicker.vue +182 -0
  51. package/src/components/ResultPanel.vue +88 -0
  52. package/src/components/ReviewPanel.vue +69 -0
  53. package/src/components/TermsDialog.vue +81 -0
  54. package/src/components/TokenDeployerView.vue +115 -0
  55. package/src/components/WalletBar.vue +108 -0
  56. package/src/components/WalrusBlobBrowser.vue +188 -0
  57. package/src/components/WalrusIconUpload.vue +205 -0
  58. package/src/composables/useAccessGate.ts +122 -0
  59. package/src/composables/useColorMode.ts +76 -0
  60. package/src/composables/useWalrusRelay.ts +21 -0
  61. package/src/config.ts +185 -0
  62. package/src/index.ts +1 -0
  63. package/src/lib/accessGate.ts +141 -0
  64. package/src/lib/buildPublishTx.ts +192 -0
  65. package/src/lib/deploy.ts +168 -0
  66. package/src/lib/deployExecutor.ts +61 -0
  67. package/src/lib/errors.ts +21 -0
  68. package/src/lib/form.ts +75 -0
  69. package/src/lib/generatePackage.ts +312 -0
  70. package/src/lib/github.ts +146 -0
  71. package/src/lib/licenses.ts +122 -0
  72. package/src/lib/readClient.ts +17 -0
  73. package/src/lib/template.ts +131 -0
  74. package/src/lib/types.ts +69 -0
  75. package/src/lib/validation.ts +172 -0
  76. package/src/lib/walrus-constants.ts +15 -0
  77. package/src/lib/walrus.ts +144 -0
  78. package/src/main.ts +154 -0
  79. package/src/move-template/sui_token_template.mv +0 -0
  80. package/src/move-template/template.ts +31 -0
  81. package/src/styles.css +384 -0
  82. package/src/template-src/files.json +10 -0
  83. package/test-results/.last-run.json +4 -0
  84. package/tests/accessGate.test.ts +108 -0
  85. package/tests/buildPublishTx.test.ts +243 -0
  86. package/tests/deploy.test.ts +81 -0
  87. package/tests/errors.test.ts +32 -0
  88. package/tests/generatePackage.test.ts +182 -0
  89. package/tests/github.test.ts +61 -0
  90. package/tests/licenses.test.ts +78 -0
  91. package/tests/template.test.ts +93 -0
  92. package/tests/templateArtifact.test.ts +61 -0
  93. package/tests/templateParity.test.ts +55 -0
  94. package/tests/useAccessGate.test.ts +101 -0
  95. package/tests/useWallet.test.ts +74 -0
  96. package/tests/useWalrusRelayGating.test.ts +30 -0
  97. package/tests/validation.test.ts +117 -0
  98. package/tests/walrusUpload.test.ts +93 -0
  99. package/tests/walrus_url_patch.test.ts +26 -0
  100. package/tsconfig.json +19 -0
  101. package/typedoc.json +34 -0
  102. package/vite.config.ts +65 -0
  103. package/vitest.config.ts +10 -0
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import { computed, reactive } from 'vue'
3
+ import type { FormModel } from '../lib/form.js'
4
+ import type { FormErrors } from '../lib/validation.js'
5
+ import { deriveStructName } from '../lib/validation.js'
6
+ import type { Network } from '../lib/types.js'
7
+ import LicensePicker from './LicensePicker.vue'
8
+ import IconPicker from './IconPicker.vue'
9
+ import FormField from './FormField.vue'
10
+ import FormSelect from './FormSelect.vue'
11
+ import FieldHint from './FieldHint.vue'
12
+
13
+ const props = defineProps<{
14
+ form: FormModel
15
+ errors: FormErrors
16
+ canProceed: boolean
17
+ network: Network
18
+ connected: boolean
19
+ }>()
20
+ const emit = defineEmits<{ (e: 'submit'): void }>()
21
+
22
+ const structPreview = computed(() => {
23
+ const m = props.form.moduleName.trim()
24
+ return m ? deriveStructName(m) : '—'
25
+ })
26
+ const coinTypePreview = computed(
27
+ () => `<package>::${props.form.moduleName.trim() || 'module'}::${structPreview.value}`,
28
+ )
29
+ const identifierError = computed(() => visibleError('packageName') || visibleError('moduleName'))
30
+
31
+ const touched = reactive(new Set<string>())
32
+ function visibleError(key: string): string | undefined {
33
+ return touched.has(key) ? props.errors[key as keyof FormErrors] : undefined
34
+ }
35
+
36
+ function onBlur(key: string): void {
37
+ touched.add(key)
38
+ const form = props.form as unknown as Record<string, unknown>
39
+ if (typeof form[key] === 'string') form[key] = (form[key] as string).trim()
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <form novalidate @submit.prevent="emit('submit')">
45
+ <fieldset>
46
+ <legend>Package identity <span class="permanent">(permanent)</span></legend>
47
+ <div class="grid2">
48
+ <FormField id="package" label="Package name" :error="visibleError('packageName')" v-slot="{ attrs }">
49
+ <input v-bind="attrs" v-model="form.packageName" placeholder="my_token" autocomplete="off" @blur="onBlur('packageName')" />
50
+ </FormField>
51
+ <FormField id="module" label="Module name" :error="visibleError('moduleName')" v-slot="{ attrs }">
52
+ <input v-bind="attrs" v-model="form.moduleName" placeholder="mytoken" autocomplete="off" @blur="onBlur('moduleName')" />
53
+ </FormField>
54
+ </div>
55
+ <p class="hint" aria-live="polite">
56
+ <template v-if="identifierError">
57
+ Fix the package and module names to preview the coin type.
58
+ </template>
59
+ <template v-else>
60
+ Coin type will be <span class="mono">{{ coinTypePreview }}</span
61
+ >. The witness struct <span class="mono">{{ structPreview }}</span> is derived from
62
+ the module name (a Sui requirement).
63
+ </template>
64
+ </p>
65
+ </fieldset>
66
+
67
+ <fieldset>
68
+ <legend>Token details</legend>
69
+
70
+ <div class="grid2">
71
+ <FormField id="name" label="Token name" :error="visibleError('name')" v-slot="{ attrs }">
72
+ <input v-bind="attrs" v-model="form.name" autocomplete="off" @blur="onBlur('name')" />
73
+ </FormField>
74
+ <FormField id="symbol" label="Symbol" :error="visibleError('symbol')" v-slot="{ attrs }">
75
+ <input v-bind="attrs" v-model="form.symbol" autocomplete="off" @blur="onBlur('symbol')" />
76
+ </FormField>
77
+ </div>
78
+
79
+ <FormField id="description" label="Description" :error="visibleError('description')">
80
+ <template #label-suffix>&nbsp;<span class="hint">(optional)</span></template>
81
+ <template #default="{ attrs }">
82
+ <textarea v-bind="attrs" v-model="form.description" @blur="onBlur('description')" />
83
+ </template>
84
+ </FormField>
85
+
86
+ <FormField id="decimals" label="Decimals" :error="visibleError('decimals')">
87
+ <template #label-suffix>&nbsp;<span class="permanent">(permanent)</span></template>
88
+ <template #default="{ attrs }">
89
+ <input v-bind="attrs" v-model="form.decimals" inputmode="numeric" @blur="onBlur('decimals')" />
90
+ </template>
91
+ </FormField>
92
+
93
+ <IconPicker
94
+ v-model="form.iconUrl"
95
+ :network="network"
96
+ :connected="connected"
97
+ :error="visibleError('iconUrl')"
98
+ @blur="onBlur('iconUrl')"
99
+ />
100
+ </fieldset>
101
+
102
+ <details>
103
+ <summary>Advanced options</summary>
104
+ <fieldset>
105
+ <legend class="visually-hidden">Advanced options</legend>
106
+
107
+ <FormField id="supply" label="Initial supply" :error="visibleError('initialSupply')">
108
+ <template #label-suffix>
109
+ <FieldHint field-id="supply">
110
+ The number of whole tokens to mint and send to the recipient at deployment.
111
+ Uses your chosen decimal precision — with 9 decimals, 1 token = 1,000,000,000 base units on-chain.
112
+ Enter <b>0</b> or leave blank for no initial supply.
113
+ You can mint more later if you choose the Mintable supply policy.
114
+ </FieldHint>
115
+ </template>
116
+ <template #default="{ attrs }">
117
+ <input v-bind="attrs" v-model="form.initialSupply" inputmode="numeric" placeholder="0" @blur="onBlur('initialSupply')" />
118
+ </template>
119
+ </FormField>
120
+
121
+ <div class="grid2">
122
+ <FormField id="supply-policy" label="Supply">
123
+ <template #label-suffix>
124
+ <FieldHint field-id="supply-policy">
125
+ <b>Mintable</b>: you keep the TreasuryCap and can mint more tokens at any time.<br />
126
+ <b>Fixed</b>: the TreasuryCap is frozen — the total supply is sealed at deployment.
127
+ </FieldHint>
128
+ </template>
129
+ <template #default="{ attrs }">
130
+ <FormSelect v-bind="attrs" v-model="form.supplyPolicy">
131
+ <option value="mintable">Mintable</option>
132
+ <option value="fixed">Fixed</option>
133
+ </FormSelect>
134
+ </template>
135
+ </FormField>
136
+ <FormField id="metadata-policy" label="Metadata">
137
+ <template #label-suffix>
138
+ <FieldHint field-id="metadata-policy">
139
+ <b>Updatable</b>: you keep the MetadataCap and can later change the name, symbol,
140
+ description, or icon.<br />
141
+ <b>Frozen</b>: the MetadataCap is discarded — all metadata is permanently locked.
142
+ </FieldHint>
143
+ </template>
144
+ <template #default="{ attrs }">
145
+ <FormSelect v-bind="attrs" v-model="form.metadataPolicy">
146
+ <option value="updatable">Updatable</option>
147
+ <option value="frozen">Frozen</option>
148
+ </FormSelect>
149
+ </template>
150
+ </FormField>
151
+ </div>
152
+
153
+ <FormField id="package-policy" label="Package code">
154
+ <template #label-suffix>
155
+ <FieldHint field-id="package-policy">
156
+ <b>Immutable</b>: the UpgradeCap is burned — the on-chain code is sealed.
157
+ Recommended for trust and auditability.<br />
158
+ <b>Upgradeable</b>: you keep the UpgradeCap and can push code changes to the package
159
+ after deployment.
160
+ </FieldHint>
161
+ </template>
162
+ <template #default="{ attrs }">
163
+ <FormSelect v-bind="attrs" v-model="form.packagePolicy">
164
+ <option value="immutable">Immutable</option>
165
+ <option value="upgradeable">Upgradeable</option>
166
+ </FormSelect>
167
+ </template>
168
+ </FormField>
169
+
170
+ <FormField id="recipient" label="Recipient" :error="visibleError('recipient')">
171
+ <template #label-suffix>
172
+ <FieldHint field-id="recipient">
173
+ The Sui address that receives the TreasuryCap, MetadataCap, UpgradeCap (if kept),
174
+ and any initial supply minted at deployment.
175
+ Leave blank to send everything to the connected wallet.
176
+ Enter a different address to separate the deployer wallet from the custody wallet.
177
+ </FieldHint>
178
+ </template>
179
+ <template #default="{ attrs }">
180
+ <input v-bind="attrs" v-model="form.recipient" placeholder="0x…" autocomplete="off" @blur="onBlur('recipient')" />
181
+ </template>
182
+ </FormField>
183
+
184
+ <LicensePicker v-model="form.license" @name="form.licenseName = $event" />
185
+ </fieldset>
186
+ </details>
187
+
188
+ <div class="row" style="margin-top: 1rem">
189
+ <button type="submit" class="primary" :disabled="!canProceed">Review &amp; deploy</button>
190
+ </div>
191
+ </form>
192
+ </template>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { DeployStep } from '../lib/deploy.js'
3
+
4
+ const ORDER: { key: DeployStep; label: string }[] = [
5
+ { key: 'patching', label: 'Preparing the module' },
6
+ { key: 'publishing', label: 'Publishing (sign in your wallet)' },
7
+ { key: 'confirming', label: 'Confirming on-chain' },
8
+ { key: 'finalizing', label: 'Finalizing supply & caps (sign again)' },
9
+ { key: 'confirming-finalize', label: 'Confirming finalize' },
10
+ { key: 'done', label: 'Done' },
11
+ ]
12
+ </script>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from 'vue'
16
+
17
+ const props = defineProps<{ step: DeployStep | null }>()
18
+
19
+ const currentIndex = computed(() => ORDER.findIndex((s) => s.key === props.step))
20
+
21
+ function getState(i: number): 'done' | 'active' | 'todo' {
22
+ if (currentIndex.value < 0) return 'todo'
23
+ if (i < currentIndex.value) return 'done'
24
+ if (i === currentIndex.value) return 'active'
25
+ return 'todo'
26
+ }
27
+
28
+ const steps = computed(() =>
29
+ ORDER.map((s, i) => ({ ...s, state: getState(i) }))
30
+ )
31
+ </script>
32
+
33
+ <template>
34
+ <section class="card" aria-labelledby="progress-h">
35
+ <h2 id="progress-h">Deploying…</h2>
36
+ <ol class="steps" aria-live="polite">
37
+ <li v-for="s in steps" :key="s.key" :data-state="s.state">
38
+ <span v-if="s.state === 'active'" class="spinner" aria-hidden="true"></span>
39
+ <span v-else-if="s.state === 'done'" aria-hidden="true">✓ </span>
40
+ {{ s.label }}
41
+ <span v-if="s.state === 'active'" class="visually-hidden">(in progress)</span>
42
+ </li>
43
+ </ol>
44
+ <p class="hint">Keep this tab open and approve the wallet prompts.</p>
45
+ </section>
46
+ </template>
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ fieldId: string }>()
3
+
4
+ function onKeyDown(e: KeyboardEvent) {
5
+ if (e.key === 'Escape') (e.currentTarget as HTMLElement).blur()
6
+ }
7
+ </script>
8
+
9
+ <template>
10
+ <span class="field-hint">
11
+ <button
12
+ type="button"
13
+ class="field-hint-btn"
14
+ :aria-describedby="`${fieldId}-hint`"
15
+ aria-label="Help"
16
+ @keydown="onKeyDown"
17
+ >?</button>
18
+ <span :id="`${fieldId}-hint`" role="tooltip" class="field-hint-tip">
19
+ <slot />
20
+ </span>
21
+ </span>
22
+ </template>
23
+
24
+ <style scoped>
25
+ .field-hint {
26
+ position: relative;
27
+ display: inline-block;
28
+ margin-left: 0.35rem;
29
+ vertical-align: middle;
30
+ }
31
+
32
+ .field-hint-btn {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ width: 1.1em;
37
+ height: 1.1em;
38
+ padding: 0;
39
+ font-size: 0.75rem;
40
+ font-weight: 700;
41
+ line-height: 1;
42
+ border-radius: 50%;
43
+ border: 1px solid var(--muted);
44
+ color: var(--muted);
45
+ background: transparent;
46
+ cursor: help;
47
+ }
48
+
49
+ .field-hint-tip {
50
+ position: absolute;
51
+ top: calc(100% + 6px);
52
+ left: 0;
53
+ z-index: 10;
54
+ min-width: 200px;
55
+ max-width: min(300px, 90vw);
56
+ padding: 0.6rem 0.75rem;
57
+ background: var(--surface);
58
+ border: 1px solid var(--border);
59
+ border-radius: 8px;
60
+ font-size: 0.85rem;
61
+ font-weight: 400;
62
+ line-height: 1.5;
63
+ color: var(--text);
64
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
65
+ visibility: hidden;
66
+ opacity: 0;
67
+ transition:
68
+ opacity 0.15s ease,
69
+ visibility 0.15s ease;
70
+ pointer-events: none;
71
+ }
72
+
73
+ .field-hint:hover .field-hint-tip,
74
+ .field-hint:focus-within .field-hint-tip {
75
+ visibility: visible;
76
+ opacity: 1;
77
+ pointer-events: auto;
78
+ }
79
+ </style>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ id: string
6
+ label: string
7
+ error?: string | null
8
+ hint?: string
9
+ }>()
10
+
11
+ const errorId = computed(() => `${props.id}-err`)
12
+ const slotAttrs = computed(() => ({
13
+ id: props.id,
14
+ 'aria-invalid': props.error ? (true as const) : undefined,
15
+ 'aria-describedby': props.error ? errorId.value : undefined,
16
+ }))
17
+ </script>
18
+
19
+ <template>
20
+ <div class="form-field">
21
+ <label :for="id">
22
+ {{ label }}<slot name="label-suffix" />
23
+ </label>
24
+ <slot :attrs="slotAttrs" />
25
+ <p v-if="hint && !error" class="hint">{{ hint }}</p>
26
+ <p v-if="error" :id="errorId" class="field-error">{{ error }}</p>
27
+ </div>
28
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ defineOptions({ inheritAttrs: false })
3
+ const model = defineModel<string>()
4
+ </script>
5
+
6
+ <template>
7
+ <div class="select-wrapper">
8
+ <select v-bind="$attrs" v-model="model"><slot /></select>
9
+ <svg
10
+ class="select-chevron"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ width="12"
13
+ height="8"
14
+ viewBox="0 0 12 8"
15
+ aria-hidden="true"
16
+ >
17
+ <path d="M1 1l5 5 5-5" />
18
+ </svg>
19
+ </div>
20
+ </template>
21
+
22
+ <style scoped>
23
+ .select-wrapper {
24
+ position: relative;
25
+ }
26
+
27
+ .select-wrapper select {
28
+ appearance: none;
29
+ padding-right: 2.5rem;
30
+ width: 100%;
31
+ }
32
+
33
+ .select-chevron {
34
+ position: absolute;
35
+ right: 0.75rem;
36
+ top: 50%;
37
+ transform: translateY(-50%);
38
+ pointer-events: none;
39
+ stroke: var(--muted);
40
+ stroke-width: 1.5;
41
+ fill: none;
42
+ }
43
+ </style>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import type { PublishResult, TokenConfig } from '../lib/types.js'
4
+ import AppNotice from './AppNotice.vue'
5
+ import { buildPackageFiles } from '../lib/generatePackage.js'
6
+ import { fetchLicenseText } from '../lib/licenses.js'
7
+ import { createRepoAndPush } from '../lib/github.js'
8
+
9
+ const props = defineProps<{ config: TokenConfig; result: PublishResult }>()
10
+
11
+ const open = ref(false)
12
+ const token = ref('')
13
+ const repoName = ref(props.config.packageName)
14
+ const isPrivate = ref(false)
15
+ const busy = ref(false)
16
+ const error = ref<string | null>(null)
17
+ const doneUrl = ref<string | null>(null)
18
+ const progress = ref('')
19
+
20
+ async function push(): Promise<void> {
21
+ busy.value = true
22
+ error.value = null
23
+ doneUrl.value = null
24
+ try {
25
+ const licenseText = await fetchLicenseText(props.config.license)
26
+ const files = buildPackageFiles({
27
+ config: props.config,
28
+ licenseText: licenseText ?? undefined,
29
+ result: props.result,
30
+ })
31
+ const res = await createRepoAndPush({
32
+ token: token.value.trim(),
33
+ repoName: repoName.value.trim(),
34
+ description: `${props.config.name} (${props.config.symbol}) — Sui coin package`,
35
+ isPrivate: isPrivate.value,
36
+ files,
37
+ commitMessage: 'Initial token package (generated by Token Deployer)',
38
+ onProgress: (d, t) => (progress.value = `Pushing files… ${d}/${t}`),
39
+ })
40
+ doneUrl.value = res.htmlUrl
41
+ } catch (e) {
42
+ error.value = e instanceof Error ? e.message : String(e)
43
+ } finally {
44
+ // Never retain the token beyond a single attempt — clear it whether the push
45
+ // succeeded or failed (a failed push previously left it in the input/memory).
46
+ token.value = ''
47
+ busy.value = false
48
+ progress.value = ''
49
+ }
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <div>
55
+ <button type="button" :aria-expanded="open" @click="open = !open">
56
+ {{ open ? '▾' : '▸' }} Create a GitHub repo (optional)
57
+ </button>
58
+
59
+ <div v-if="open" class="card" style="margin-top: 0.5rem">
60
+ <p v-once class="hint">
61
+ Uses your <strong>own</strong> GitHub token — the repo is created in your account and this
62
+ site's servers are never involved. The token is sent only to
63
+ <span class="mono">api.github.com</span> over HTTPS, is never stored or logged, and is
64
+ cleared as soon as the push finishes. Use a fine-grained token with "Contents" and
65
+ "Administration" write access, or a classic token with the
66
+ <span class="mono">repo</span> scope.
67
+ </p>
68
+
69
+ <label for="gh-repo">Repository name</label>
70
+ <input id="gh-repo" v-model="repoName" autocomplete="off" />
71
+
72
+ <label for="gh-token">GitHub token</label>
73
+ <input
74
+ id="gh-token"
75
+ v-model="token"
76
+ type="password"
77
+ autocomplete="off"
78
+ spellcheck="false"
79
+ placeholder="github_pat_… or ghp_…"
80
+ />
81
+
82
+ <label style="font-weight: 400; margin-top: 0.75rem">
83
+ <input type="checkbox" v-model="isPrivate" style="width: auto" /> Make the repository
84
+ private
85
+ </label>
86
+
87
+ <div class="row" style="margin-top: 0.75rem">
88
+ <button type="button" class="primary" :disabled="busy || !token || !repoName" @click="push">
89
+ <span v-if="busy" class="spinner" aria-hidden="true"></span> Create repo &amp; push
90
+ </button>
91
+ <span v-if="progress" class="hint" aria-live="polite">{{ progress }}</span>
92
+ </div>
93
+
94
+ <AppNotice v-if="error" type="error">{{ error }}</AppNotice>
95
+ <AppNotice v-if="doneUrl" type="ok">
96
+ Pushed: <a :href="doneUrl" target="_blank" rel="noopener noreferrer">{{ doneUrl }}</a>
97
+ </AppNotice>
98
+ </div>
99
+ </div>
100
+ </template>