vue-kaspa-cli 0.1.9 → 0.1.11

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": "vue-kaspa-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Vue 3 or Nuxt app wired to the Kaspa blockchain",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@vue-kaspa/kaspa-wasm": "latest",
13
+ "lucide-vue-next": "latest",
13
14
  "nuxt": "latest",
14
15
  "vue-kaspa": "latest"
15
16
  }
@@ -31,7 +31,12 @@
31
31
  body {
32
32
  margin: 0;
33
33
  min-height: 100vh;
34
- background: var(--ks-surface);
34
+ background:
35
+ radial-gradient(ellipse 60% 55% at 8% 100%, rgba(73, 197, 163, .32) 0%, transparent 55%),
36
+ radial-gradient(ellipse 60% 55% at 92% 100%, rgba(73, 197, 163, .32) 0%, transparent 55%),
37
+ radial-gradient(ellipse 80% 28% at 50% 100%, rgba(73, 197, 163, .18) 0%, transparent 60%),
38
+ var(--ks-surface);
39
+ background-attachment: fixed;
35
40
  color: var(--ks-text);
36
41
  font-family: Inter, system-ui, -apple-system, sans-serif;
37
42
  }
@@ -1,11 +1,21 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, onMounted, onUnmounted } from 'vue'
3
- // useKaspa, useRpc, and computed are auto-imported by Nuxt.
3
+ import { Droplet, BookOpen, Search, ArrowUpRight, Heart, Copy, Check } from 'lucide-vue-next'
4
+ // useKaspa and useRpc are auto-imported by Nuxt.
4
5
 
5
6
  const kaspa = useKaspa()
6
7
  const rpc = useRpc()
7
8
  const bento = ref<HTMLElement | null>(null)
8
9
  const donateDialog = ref<HTMLDialogElement | null>(null)
10
+ const copied = ref(false)
11
+
12
+ const KASPA_ADDRESS = 'kaspa:qypr7ayn2g55fccyv9n6gf9zgrcnpepkfgjf9d8mtfp68ezv3mgqnggxqs902q4'
13
+
14
+ async function copyAddress() {
15
+ await navigator.clipboard.writeText(KASPA_ADDRESS)
16
+ copied.value = true
17
+ setTimeout(() => { copied.value = false }, 2000)
18
+ }
9
19
 
10
20
  const stateLabel = computed(() => {
11
21
  if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
@@ -31,12 +41,12 @@ const daaScore = computed(() =>
31
41
  )
32
42
 
33
43
  const links = [
34
- { label: 'Faucet', title: 'Testnet 10', desc: 'Get free test KAS', icon: '💧', href: 'https://faucet-tn10.kaspanet.io/' },
35
- { label: 'Faucet', title: 'Testnet 12', desc: 'Get free test KAS', icon: '💧', href: 'https://faucet-tn12.kaspanet.io/' },
36
- { label: 'Docs', title: 'vue-kaspa', desc: 'Read the full docs', icon: '📖', href: 'https://vue-kaspa.vercel.app/' },
37
- { label: 'Explorer', title: 'Testnet 10', desc: 'Browse transactions', icon: '🔍', href: 'https://tn10.kaspa.stream/' },
38
- { label: 'Explorer', title: 'Testnet 12', desc: 'Browse transactions', icon: '🔍', href: 'https://tn12.kaspa.stream/' },
39
- { label: 'Explorer', title: 'Mainnet', desc: 'Browse transactions', icon: '🔍', href: 'https://kaspa.stream/' },
44
+ { label: 'Faucet', title: 'Testnet 10', desc: 'Get free test KAS', icon: Droplet, href: 'https://faucet-tn10.kaspanet.io/' },
45
+ { label: 'Faucet', title: 'Testnet 12', desc: 'Get free test KAS', icon: Droplet, href: 'https://faucet-tn12.kaspanet.io/' },
46
+ { label: 'Docs', title: 'vue-kaspa', desc: 'Read the full docs', icon: BookOpen, href: 'https://vue-kaspa.vercel.app/' },
47
+ { label: 'Explorer', title: 'Testnet 10', desc: 'Browse transactions', icon: Search, href: 'https://tn10.kaspa.stream/' },
48
+ { label: 'Explorer', title: 'Testnet 12', desc: 'Browse transactions', icon: Search, href: 'https://tn12.kaspa.stream/' },
49
+ { label: 'Explorer', title: 'Mainnet', desc: 'Browse transactions', icon: Search, href: 'https://kaspa.stream/' },
40
50
  ]
41
51
 
42
52
  function onMouseMove(e: MouseEvent) {
@@ -59,7 +69,14 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
59
69
  <button class="ks-dialog-close" @click="donateDialog?.close()">✕</button>
60
70
  <p class="ks-dialog-title">Support vue-kaspa ❤️</p>
61
71
  <p class="ks-dialog-body">vue-kaspa is free and open-source. If it saves you time, consider sending some KAS — every bit helps keep the project alive and maintained.</p>
62
- <code class="ks-dialog-addr">kaspa:qypr7ayn2g55fccyv9n6gf9zgrcnpepkfgjf9d8mtfp68ezv3mgqnggxqs902q4</code>
72
+ <div class="ks-copy-wrap">
73
+ <code class="ks-dialog-addr">{{ KASPA_ADDRESS }}</code>
74
+ <button class="ks-copy-btn" :class="{ copied }" @click="copyAddress">
75
+ <Check v-if="copied" :size="13" />
76
+ <Copy v-else :size="13" />
77
+ {{ copied ? 'Copied!' : 'Copy' }}
78
+ </button>
79
+ </div>
63
80
  <p class="ks-dialog-thanks">Thank you for your support 🙏</p>
64
81
  </div>
65
82
  </dialog>
@@ -83,74 +100,72 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
83
100
  </svg>
84
101
  </a>
85
102
  <button class="ks-icon-btn" title="Support this project" @click="donateDialog?.showModal()">
86
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
87
- <path d="M12 21.593c-.425-.396-8.8-8.044-8.8-12.593C3.2 5.796 7.192 3 12 3s8.8 2.796 8.8 6c0 4.549-8.375 12.197-8.8 12.593z"/>
88
- </svg>
103
+ <Heart :size="18" />
89
104
  </button>
90
105
  </nav>
91
106
  </header>
92
107
 
93
- <!-- Bento grid — Γ layout: net card spans col 1–2 × row 1–3, links fill col 3 then bottom row -->
108
+ <!-- Bento grid -->
94
109
  <div ref="bento" class="ks-grid">
95
110
 
96
- <!-- Network card -->
97
- <div data-shine class="ks-shine ks-net-shine">
98
- <div class="ks-card ks-net-card">
99
- <div class="ks-net-top">
100
- <span class="ks-net-icon">⬡</span>
101
- <span
102
- class="ks-badge"
103
- :style="`border-color:${badgeColor};color:${badgeColor}`"
104
- >{{ stateLabel }}</span>
105
- </div>
106
- <div class="ks-stats">
107
- <div class="ks-stat">
108
- <span class="ks-stat-label">Network</span>
109
- <span class="ks-stat-value">{{ rpc.networkId.value ?? '—' }}</span>
110
- </div>
111
- <div class="ks-stat">
112
- <span class="ks-stat-label">Server version</span>
113
- <span class="ks-stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
114
- </div>
115
- <div class="ks-stat">
116
- <span class="ks-stat-label">DAA Score</span>
117
- <span class="ks-stat-value" style="font-family:monospace">{{ daaScore }}</span>
118
- </div>
119
- <div class="ks-stat">
120
- <span class="ks-stat-label">Synced</span>
111
+ <!-- Network card: col 1–2, row 1–3 -->
112
+ <div data-shine class="ks-shine ks-net-shine">
113
+ <div class="ks-card ks-net-card">
114
+ <div class="ks-net-top">
115
+ <span class="ks-net-icon">⬡</span>
121
116
  <span
122
- class="ks-stat-value"
123
- :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
124
- >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
117
+ class="ks-badge"
118
+ :style="`border-color:${badgeColor};color:${badgeColor}`"
119
+ >{{ stateLabel }}</span>
120
+ </div>
121
+ <div class="ks-stats">
122
+ <div class="ks-stat">
123
+ <span class="ks-stat-label">Network</span>
124
+ <span class="ks-stat-value">{{ rpc.networkId.value ?? '—' }}</span>
125
+ </div>
126
+ <div class="ks-stat">
127
+ <span class="ks-stat-label">Server version</span>
128
+ <span class="ks-stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
129
+ </div>
130
+ <div class="ks-stat">
131
+ <span class="ks-stat-label">DAA Score</span>
132
+ <span class="ks-stat-value" style="font-family:monospace">{{ daaScore }}</span>
133
+ </div>
134
+ <div class="ks-stat">
135
+ <span class="ks-stat-label">Synced</span>
136
+ <span
137
+ class="ks-stat-value"
138
+ :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
139
+ >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
140
+ </div>
125
141
  </div>
126
142
  </div>
127
143
  </div>
128
- </div>
129
144
 
130
- <!-- Link cards auto-placed: first 3 fill col 3 rows 1–3, last 3 fill row 4 -->
131
- <a
132
- v-for="link in links"
133
- :key="link.href"
134
- data-shine
135
- class="ks-shine ks-link-shine"
136
- :href="link.href"
137
- target="_blank"
138
- rel="noopener"
139
- >
140
- <div class="ks-card ks-link-card">
141
- <div class="ks-link-top">
142
- <span class="ks-link-icon">{{ link.icon }}</span>
143
- <span class="ks-link-arrow">↗</span>
144
- </div>
145
- <div>
146
- <div class="ks-link-label">{{ link.label }}</div>
147
- <div class="ks-link-title">{{ link.title }}</div>
148
- <div class="ks-link-desc">{{ link.desc }}</div>
145
+ <!-- Link cards: auto-placed into Γ shape -->
146
+ <a
147
+ v-for="link in links"
148
+ :key="link.href"
149
+ data-shine
150
+ class="ks-shine ks-link-shine"
151
+ :href="link.href"
152
+ target="_blank"
153
+ rel="noopener"
154
+ >
155
+ <div class="ks-card ks-link-card">
156
+ <div class="ks-link-top">
157
+ <component :is="link.icon" :size="20" class="ks-link-icon" />
158
+ <ArrowUpRight :size="13" class="ks-link-arrow" />
159
+ </div>
160
+ <div>
161
+ <div class="ks-link-label">{{ link.label }}</div>
162
+ <div class="ks-link-title">{{ link.title }}</div>
163
+ <div class="ks-link-desc">{{ link.desc }}</div>
164
+ </div>
149
165
  </div>
150
- </div>
151
- </a>
166
+ </a>
152
167
 
153
- </div>
168
+ </div>
154
169
  </div>
155
170
  </template>
156
171
 
@@ -163,15 +178,10 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
163
178
  }
164
179
 
165
180
  /* Header */
166
- .ks-header {
167
- display: flex;
168
- align-items: center;
169
- gap: 0.6rem;
170
- margin-bottom: 0.75rem;
171
- }
181
+ .ks-header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
172
182
  .ks-header-logo { width: 28px; height: 28px; object-fit: contain; }
173
183
  .ks-header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
174
- .ks-header-nav { display: flex; gap: 0.15rem; }
184
+ .ks-header-nav { display: flex; gap: .15rem; }
175
185
 
176
186
  .ks-icon-btn {
177
187
  display: inline-flex;
@@ -185,7 +195,7 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
185
195
  color: var(--ks-muted);
186
196
  cursor: pointer;
187
197
  text-decoration: none;
188
- transition: color 0.15s, background 0.15s;
198
+ transition: color .15s, background .15s;
189
199
  }
190
200
  .ks-icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
191
201
 
@@ -193,10 +203,10 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
193
203
  .ks-grid {
194
204
  display: grid;
195
205
  grid-template-columns: repeat(3, 1fr);
196
- gap: 0.75rem;
206
+ gap: .75rem;
197
207
  }
198
208
 
199
- /* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
209
+ /* Shine wrapper */
200
210
  .ks-shine {
201
211
  padding: 1px;
202
212
  border-radius: 14px;
@@ -208,19 +218,10 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
208
218
  display: block;
209
219
  text-decoration: none;
210
220
  }
221
+ .ks-net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
211
222
 
212
- /* Network card occupies col 1–2, row 1–3 */
213
- .ks-net-shine {
214
- grid-column: 1 / span 2;
215
- grid-row: 1 / span 3;
216
- }
217
-
218
- /* Card base — solid fill sits inside the 1px shine gap */
219
- .ks-card {
220
- border-radius: 13px;
221
- background: var(--ks-soft);
222
- height: 100%;
223
- }
223
+ /* Card base */
224
+ .ks-card { border-radius: 13px; background: var(--ks-soft); height: 100%; }
224
225
 
225
226
  /* Network card */
226
227
  .ks-net-card {
@@ -242,19 +243,8 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
242
243
  }
243
244
  .ks-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
244
245
  .ks-stat { display: flex; flex-direction: column; gap: .25rem; }
245
- .ks-stat-label {
246
- font-size: .65rem;
247
- text-transform: uppercase;
248
- letter-spacing: .06em;
249
- color: var(--ks-muted);
250
- }
251
- .ks-stat-value {
252
- font-size: .95rem;
253
- color: var(--ks-heading);
254
- overflow: hidden;
255
- text-overflow: ellipsis;
256
- white-space: nowrap;
257
- }
246
+ .ks-stat-label { font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); }
247
+ .ks-stat-value { font-size: .95rem; color: var(--ks-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
258
248
 
259
249
  /* Link cards */
260
250
  .ks-link-card {
@@ -262,19 +252,13 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
262
252
  display: flex;
263
253
  flex-direction: column;
264
254
  justify-content: space-between;
265
- gap: 0.5rem;
255
+ gap: .5rem;
266
256
  min-height: 100px;
267
257
  }
268
258
  .ks-link-top { display: flex; justify-content: space-between; align-items: flex-start; }
269
- .ks-link-icon { font-size: 1.35rem; line-height: 1; }
270
- .ks-link-arrow { font-size: .75rem; color: var(--ks-muted); }
271
- .ks-link-label {
272
- font-size: .58rem;
273
- text-transform: uppercase;
274
- letter-spacing: .06em;
275
- color: var(--ks-muted);
276
- margin-bottom: .1rem;
277
- }
259
+ .ks-link-icon { color: var(--ks-accent); }
260
+ .ks-link-arrow { color: var(--ks-muted); }
261
+ .ks-link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
278
262
  .ks-link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
279
263
  .ks-link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
280
264
 
@@ -283,8 +267,9 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
283
267
  border: 1px solid var(--ks-border);
284
268
  border-radius: 16px;
285
269
  padding: 0;
286
- max-width: 420px;
287
- width: calc(100vw - 2rem);
270
+ width: min(440px, calc(100vw - 2rem));
271
+ max-width: none;
272
+ overflow: hidden;
288
273
  background: var(--ks-surface);
289
274
  color: var(--ks-text);
290
275
  box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
@@ -305,6 +290,12 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
305
290
  .ks-dialog-close:hover { color: var(--ks-heading); }
306
291
  .ks-dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
307
292
  .ks-dialog-body { font-size: .875rem; color: var(--ks-muted); margin: 0 0 1.25rem; line-height: 1.65; }
293
+
294
+ .ks-copy-wrap {
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: .5rem;
298
+ }
308
299
  .ks-dialog-addr {
309
300
  display: block;
310
301
  padding: .6em .85em;
@@ -313,13 +304,31 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
313
304
  border: 1px solid var(--ks-border);
314
305
  font-size: .7rem;
315
306
  word-break: break-all;
307
+ overflow-wrap: anywhere;
308
+ white-space: normal;
316
309
  color: var(--ks-text);
317
310
  }
311
+ .ks-copy-btn {
312
+ display: inline-flex;
313
+ align-items: center;
314
+ gap: .3rem;
315
+ align-self: flex-end;
316
+ padding: .35em .75em;
317
+ border-radius: 6px;
318
+ border: 1px solid var(--ks-border);
319
+ background: var(--ks-soft);
320
+ color: var(--ks-muted);
321
+ font-size: .75rem;
322
+ cursor: pointer;
323
+ transition: color .15s, border-color .15s;
324
+ }
325
+ .ks-copy-btn:hover { color: var(--ks-heading); border-color: var(--ks-heading); }
326
+ .ks-copy-btn.copied { color: #4caf50; border-color: #4caf50; }
327
+
318
328
  .ks-dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
319
329
  </style>
320
330
 
321
331
  <style>
322
- /* ::backdrop can't receive scoped attribute — must be global */
323
332
  .ks-dialog::backdrop {
324
333
  background: rgba(0, 0, 0, .5);
325
334
  backdrop-filter: blur(4px);
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@vue-kaspa/kaspa-wasm": "latest",
14
+ "lucide-vue-next": "latest",
14
15
  "vue": "latest",
15
16
  "vue-kaspa": "latest"
16
17
  },
@@ -1,11 +1,21 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import { useKaspa, useRpc } from 'vue-kaspa'
4
+ import { Droplet, BookOpen, Search, ArrowUpRight, Heart, Copy, Check } from 'lucide-vue-next'
4
5
 
5
6
  const kaspa = useKaspa()
6
7
  const rpc = useRpc()
7
8
  const bento = ref<HTMLElement | null>(null)
8
9
  const donateDialog = ref<HTMLDialogElement | null>(null)
10
+ const copied = ref(false)
11
+
12
+ const KASPA_ADDRESS = 'kaspa:qypr7ayn2g55fccyv9n6gf9zgrcnpepkfgjf9d8mtfp68ezv3mgqnggxqs902q4'
13
+
14
+ async function copyAddress() {
15
+ await navigator.clipboard.writeText(KASPA_ADDRESS)
16
+ copied.value = true
17
+ setTimeout(() => { copied.value = false }, 2000)
18
+ }
9
19
 
10
20
  const stateLabel = computed(() => {
11
21
  if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
@@ -31,12 +41,12 @@ const daaScore = computed(() =>
31
41
  )
32
42
 
33
43
  const links = [
34
- { label: 'Faucet', title: 'Testnet 10', desc: 'Get free test KAS', icon: '💧', href: 'https://faucet-tn10.kaspanet.io/' },
35
- { label: 'Faucet', title: 'Testnet 12', desc: 'Get free test KAS', icon: '💧', href: 'https://faucet-tn12.kaspanet.io/' },
36
- { label: 'Docs', title: 'vue-kaspa', desc: 'Read the full docs', icon: '📖', href: 'https://vue-kaspa.vercel.app/' },
37
- { label: 'Explorer', title: 'Testnet 10', desc: 'Browse transactions', icon: '🔍', href: 'https://tn10.kaspa.stream/' },
38
- { label: 'Explorer', title: 'Testnet 12', desc: 'Browse transactions', icon: '🔍', href: 'https://tn12.kaspa.stream/' },
39
- { label: 'Explorer', title: 'Mainnet', desc: 'Browse transactions', icon: '🔍', href: 'https://kaspa.stream/' },
44
+ { label: 'Faucet', title: 'Testnet 10', desc: 'Get free test KAS', icon: Droplet, href: 'https://faucet-tn10.kaspanet.io/' },
45
+ { label: 'Faucet', title: 'Testnet 12', desc: 'Get free test KAS', icon: Droplet, href: 'https://faucet-tn12.kaspanet.io/' },
46
+ { label: 'Docs', title: 'vue-kaspa', desc: 'Read the full docs', icon: BookOpen, href: 'https://vue-kaspa.vercel.app/' },
47
+ { label: 'Explorer', title: 'Testnet 10', desc: 'Browse transactions', icon: Search, href: 'https://tn10.kaspa.stream/' },
48
+ { label: 'Explorer', title: 'Testnet 12', desc: 'Browse transactions', icon: Search, href: 'https://tn12.kaspa.stream/' },
49
+ { label: 'Explorer', title: 'Mainnet', desc: 'Browse transactions', icon: Search, href: 'https://kaspa.stream/' },
40
50
  ]
41
51
 
42
52
  function onMouseMove(e: MouseEvent) {
@@ -59,7 +69,14 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
59
69
  <button class="dialog-close" @click="donateDialog?.close()">✕</button>
60
70
  <p class="dialog-title">Support vue-kaspa ❤️</p>
61
71
  <p class="dialog-body">vue-kaspa is free and open-source. If it saves you time, consider sending some KAS — every bit helps keep the project alive and maintained.</p>
62
- <code class="dialog-addr">kaspa:qypr7ayn2g55fccyv9n6gf9zgrcnpepkfgjf9d8mtfp68ezv3mgqnggxqs902q4</code>
72
+ <div class="copy-wrap">
73
+ <code class="dialog-addr">{{ KASPA_ADDRESS }}</code>
74
+ <button class="copy-btn" :class="{ copied }" @click="copyAddress">
75
+ <Check v-if="copied" :size="13" />
76
+ <Copy v-else :size="13" />
77
+ {{ copied ? 'Copied!' : 'Copy' }}
78
+ </button>
79
+ </div>
63
80
  <p class="dialog-thanks">Thank you for your support 🙏</p>
64
81
  </div>
65
82
  </dialog>
@@ -83,83 +100,75 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
83
100
  </svg>
84
101
  </a>
85
102
  <button class="icon-btn" title="Support this project" @click="donateDialog?.showModal()">
86
- <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
87
- <path d="M12 21.593c-.425-.396-8.8-8.044-8.8-12.593C3.2 5.796 7.192 3 12 3s8.8 2.796 8.8 6c0 4.549-8.375 12.197-8.8 12.593z"/>
88
- </svg>
103
+ <Heart :size="18" />
89
104
  </button>
90
105
  </nav>
91
106
  </header>
92
107
 
93
- <!-- Bento grid — Γ layout: net card spans col 1–2 × row 1–3, links fill col 3 then bottom row -->
108
+ <!-- Bento grid -->
94
109
  <div ref="bento" class="grid">
95
110
 
96
- <!-- Network card -->
97
- <div data-shine class="shine net-shine">
98
- <div class="card net-card">
99
- <div class="net-top">
100
- <span class="net-icon">⬡</span>
101
- <span
102
- class="badge"
103
- :style="`border-color:${badgeColor};color:${badgeColor}`"
104
- >{{ stateLabel }}</span>
105
- </div>
106
- <div class="stats">
107
- <div class="stat">
108
- <span class="stat-label">Network</span>
109
- <span class="stat-value">{{ rpc.networkId.value ?? '—' }}</span>
110
- </div>
111
- <div class="stat">
112
- <span class="stat-label">Server version</span>
113
- <span class="stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
114
- </div>
115
- <div class="stat">
116
- <span class="stat-label">DAA Score</span>
117
- <span class="stat-value mono">{{ daaScore }}</span>
111
+ <!-- Network card: col 1–2, row 1–3 -->
112
+ <div data-shine class="shine net-shine">
113
+ <div class="card net-card">
114
+ <div class="net-top">
115
+ <span class="net-icon">⬡</span>
116
+ <span class="badge" :style="`border-color:${badgeColor};color:${badgeColor}`">{{ stateLabel }}</span>
118
117
  </div>
119
- <div class="stat">
120
- <span class="stat-label">Synced</span>
121
- <span
122
- class="stat-value"
123
- :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
124
- >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
118
+ <div class="stats">
119
+ <div class="stat">
120
+ <span class="stat-label">Network</span>
121
+ <span class="stat-value">{{ rpc.networkId.value ?? '—' }}</span>
122
+ </div>
123
+ <div class="stat">
124
+ <span class="stat-label">Server version</span>
125
+ <span class="stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
126
+ </div>
127
+ <div class="stat">
128
+ <span class="stat-label">DAA Score</span>
129
+ <span class="stat-value mono">{{ daaScore }}</span>
130
+ </div>
131
+ <div class="stat">
132
+ <span class="stat-label">Synced</span>
133
+ <span
134
+ class="stat-value"
135
+ :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
136
+ >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
137
+ </div>
125
138
  </div>
126
139
  </div>
127
140
  </div>
128
- </div>
129
141
 
130
- <!-- Link cards auto-placed: first 3 fill col 3 rows 1–3, last 3 fill row 4 -->
131
- <a
132
- v-for="link in links"
133
- :key="link.href"
134
- data-shine
135
- class="shine link-shine"
136
- :href="link.href"
137
- target="_blank"
138
- rel="noopener"
139
- >
140
- <div class="card link-card">
141
- <div class="link-top">
142
- <span class="link-icon">{{ link.icon }}</span>
143
- <span class="link-arrow">↗</span>
144
- </div>
145
- <div>
146
- <div class="link-label">{{ link.label }}</div>
147
- <div class="link-title">{{ link.title }}</div>
148
- <div class="link-desc">{{ link.desc }}</div>
142
+ <!-- Link cards: auto-placed into Γ shape -->
143
+ <a
144
+ v-for="link in links"
145
+ :key="link.href"
146
+ data-shine
147
+ class="shine link-shine"
148
+ :href="link.href"
149
+ target="_blank"
150
+ rel="noopener"
151
+ >
152
+ <div class="card link-card">
153
+ <div class="link-top">
154
+ <component :is="link.icon" :size="20" class="link-icon" />
155
+ <ArrowUpRight :size="13" class="link-arrow" />
156
+ </div>
157
+ <div>
158
+ <div class="link-label">{{ link.label }}</div>
159
+ <div class="link-title">{{ link.title }}</div>
160
+ <div class="link-desc">{{ link.desc }}</div>
161
+ </div>
149
162
  </div>
150
- </div>
151
- </a>
163
+ </a>
152
164
 
153
- </div>
165
+ </div>
154
166
  </div>
155
167
  </template>
156
168
 
157
169
  <style scoped>
158
170
  /* Root */
159
- .root {
160
- width: 100%;
161
- font-family: Inter, system-ui, -apple-system, sans-serif;
162
- }
171
+ .root { width: 100%; font-family: Inter, system-ui, -apple-system, sans-serif; }
163
172
 
164
173
  /* Header */
165
174
  .header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
@@ -184,9 +193,13 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
184
193
  .icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
185
194
 
186
195
  /* Grid */
187
- .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; }
196
+ .grid {
197
+ display: grid;
198
+ grid-template-columns: repeat(3, 1fr);
199
+ gap: .75rem;
200
+ }
188
201
 
189
- /* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
202
+ /* Shine wrapper */
190
203
  .shine {
191
204
  padding: 1px;
192
205
  border-radius: 14px;
@@ -198,8 +211,6 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
198
211
  display: block;
199
212
  text-decoration: none;
200
213
  }
201
-
202
- /* Network card occupies col 1–2, row 1–3 */
203
214
  .net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
204
215
 
205
216
  /* Card base */
@@ -219,8 +230,8 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
219
230
  /* Link cards */
220
231
  .link-card { padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: .5rem; min-height: 100px; }
221
232
  .link-top { display: flex; justify-content: space-between; align-items: flex-start; }
222
- .link-icon { font-size: 1.35rem; line-height: 1; }
223
- .link-arrow { font-size: .75rem; color: var(--ks-muted); }
233
+ .link-icon { color: var(--ks-accent); }
234
+ .link-arrow { color: var(--ks-muted); }
224
235
  .link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
225
236
  .link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
226
237
  .link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
@@ -230,8 +241,9 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
230
241
  border: 1px solid var(--ks-border);
231
242
  border-radius: 16px;
232
243
  padding: 0;
233
- max-width: 420px;
234
- width: calc(100vw - 2rem);
244
+ width: min(440px, calc(100vw - 2rem));
245
+ max-width: none;
246
+ overflow: hidden;
235
247
  background: var(--ks-surface);
236
248
  color: var(--ks-text);
237
249
  box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
@@ -241,12 +253,41 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
241
253
  .dialog-close:hover { color: var(--ks-heading); }
242
254
  .dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
243
255
  .dialog-body { font-size: .875rem; color: var(--ks-muted); margin: 0 0 1.25rem; line-height: 1.65; }
244
- .dialog-addr { display: block; padding: .6em .85em; border-radius: 8px; background: var(--ks-soft); border: 1px solid var(--ks-border); font-size: .7rem; word-break: break-all; color: var(--ks-text); }
256
+
257
+ .copy-wrap { display: flex; flex-direction: column; gap: .5rem; }
258
+ .dialog-addr {
259
+ display: block;
260
+ padding: .6em .85em;
261
+ border-radius: 8px;
262
+ background: var(--ks-soft);
263
+ border: 1px solid var(--ks-border);
264
+ font-size: .7rem;
265
+ word-break: break-all;
266
+ overflow-wrap: anywhere;
267
+ white-space: normal;
268
+ color: var(--ks-text);
269
+ }
270
+ .copy-btn {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: .3rem;
274
+ align-self: flex-end;
275
+ padding: .35em .75em;
276
+ border-radius: 6px;
277
+ border: 1px solid var(--ks-border);
278
+ background: var(--ks-soft);
279
+ color: var(--ks-muted);
280
+ font-size: .75rem;
281
+ cursor: pointer;
282
+ transition: color .15s, border-color .15s;
283
+ }
284
+ .copy-btn:hover { color: var(--ks-heading); border-color: var(--ks-heading); }
285
+ .copy-btn.copied { color: #4caf50; border-color: #4caf50; }
286
+
245
287
  .dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
246
288
  </style>
247
289
 
248
290
  <style>
249
- /* ::backdrop can't receive scoped attribute — must be global */
250
291
  .dialog::backdrop {
251
292
  background: rgba(0, 0, 0, .5);
252
293
  backdrop-filter: blur(4px);
@@ -29,7 +29,12 @@ body {
29
29
  display: flex;
30
30
  align-items: center;
31
31
  justify-content: center;
32
- background: var(--ks-surface);
32
+ background:
33
+ radial-gradient(ellipse 60% 55% at 8% 100%, rgba(73, 197, 163, .32) 0%, transparent 55%),
34
+ radial-gradient(ellipse 60% 55% at 92% 100%, rgba(73, 197, 163, .32) 0%, transparent 55%),
35
+ radial-gradient(ellipse 80% 28% at 50% 100%, rgba(73, 197, 163, .18) 0%, transparent 60%),
36
+ var(--ks-surface);
37
+ background-attachment: fixed;
33
38
  color: var(--ks-text);
34
39
  font-family: Inter, system-ui, -apple-system, sans-serif;
35
40
  padding: 2rem 1.5rem;