vue-kaspa-cli 0.1.9 → 0.1.10

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.10",
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
  }
@@ -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,73 +100,73 @@ 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 -->
94
- <div ref="bento" class="ks-grid">
108
+ <!-- Gradient + bento grid -->
109
+ <div class="ks-grid-wrap">
110
+ <div ref="bento" class="ks-grid">
95
111
 
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>
112
+ <!-- Network card: col 1–2, row 1–3 -->
113
+ <div data-shine class="ks-shine ks-net-shine">
114
+ <div class="ks-card ks-net-card">
115
+ <div class="ks-net-top">
116
+ <span class="ks-net-icon">⬡</span>
121
117
  <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>
118
+ class="ks-badge"
119
+ :style="`border-color:${badgeColor};color:${badgeColor}`"
120
+ >{{ stateLabel }}</span>
121
+ </div>
122
+ <div class="ks-stats">
123
+ <div class="ks-stat">
124
+ <span class="ks-stat-label">Network</span>
125
+ <span class="ks-stat-value">{{ rpc.networkId.value ?? '—' }}</span>
126
+ </div>
127
+ <div class="ks-stat">
128
+ <span class="ks-stat-label">Server version</span>
129
+ <span class="ks-stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
130
+ </div>
131
+ <div class="ks-stat">
132
+ <span class="ks-stat-label">DAA Score</span>
133
+ <span class="ks-stat-value" style="font-family:monospace">{{ daaScore }}</span>
134
+ </div>
135
+ <div class="ks-stat">
136
+ <span class="ks-stat-label">Synced</span>
137
+ <span
138
+ class="ks-stat-value"
139
+ :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
140
+ >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
141
+ </div>
125
142
  </div>
126
143
  </div>
127
144
  </div>
128
- </div>
129
145
 
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>
146
+ <!-- Link cards: auto-placed into Γ shape -->
147
+ <a
148
+ v-for="link in links"
149
+ :key="link.href"
150
+ data-shine
151
+ class="ks-shine ks-link-shine"
152
+ :href="link.href"
153
+ target="_blank"
154
+ rel="noopener"
155
+ >
156
+ <div class="ks-card ks-link-card">
157
+ <div class="ks-link-top">
158
+ <component :is="link.icon" :size="20" class="ks-link-icon" />
159
+ <ArrowUpRight :size="13" class="ks-link-arrow" />
160
+ </div>
161
+ <div>
162
+ <div class="ks-link-label">{{ link.label }}</div>
163
+ <div class="ks-link-title">{{ link.title }}</div>
164
+ <div class="ks-link-desc">{{ link.desc }}</div>
165
+ </div>
149
166
  </div>
150
- </div>
151
- </a>
167
+ </a>
152
168
 
169
+ </div>
153
170
  </div>
154
171
  </div>
155
172
  </template>
@@ -163,15 +180,10 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
163
180
  }
164
181
 
165
182
  /* Header */
166
- .ks-header {
167
- display: flex;
168
- align-items: center;
169
- gap: 0.6rem;
170
- margin-bottom: 0.75rem;
171
- }
183
+ .ks-header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
172
184
  .ks-header-logo { width: 28px; height: 28px; object-fit: contain; }
173
185
  .ks-header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
174
- .ks-header-nav { display: flex; gap: 0.15rem; }
186
+ .ks-header-nav { display: flex; gap: .15rem; }
175
187
 
176
188
  .ks-icon-btn {
177
189
  display: inline-flex;
@@ -185,18 +197,36 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
185
197
  color: var(--ks-muted);
186
198
  cursor: pointer;
187
199
  text-decoration: none;
188
- transition: color 0.15s, background 0.15s;
200
+ transition: color .15s, background .15s;
189
201
  }
190
202
  .ks-icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
191
203
 
204
+ /* Grid wrap + U-shaped gradient backdrop */
205
+ .ks-grid-wrap {
206
+ position: relative;
207
+ }
208
+ .ks-grid-wrap::before {
209
+ content: '';
210
+ position: absolute;
211
+ inset: 0 -20px -28px;
212
+ z-index: 0;
213
+ pointer-events: none;
214
+ background:
215
+ radial-gradient(ellipse 55% 65% at 8% 100%, rgba(73, 197, 163, .38) 0%, transparent 55%),
216
+ radial-gradient(ellipse 55% 65% at 92% 100%, rgba(73, 197, 163, .38) 0%, transparent 55%),
217
+ radial-gradient(ellipse 85% 30% at 50% 100%, rgba(73, 197, 163, .22) 0%, transparent 60%);
218
+ }
219
+
192
220
  /* Grid */
193
221
  .ks-grid {
194
222
  display: grid;
195
223
  grid-template-columns: repeat(3, 1fr);
196
- gap: 0.75rem;
224
+ gap: .75rem;
225
+ position: relative;
226
+ z-index: 1;
197
227
  }
198
228
 
199
- /* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
229
+ /* Shine wrapper */
200
230
  .ks-shine {
201
231
  padding: 1px;
202
232
  border-radius: 14px;
@@ -208,19 +238,10 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
208
238
  display: block;
209
239
  text-decoration: none;
210
240
  }
241
+ .ks-net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
211
242
 
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
- }
243
+ /* Card base */
244
+ .ks-card { border-radius: 13px; background: var(--ks-soft); height: 100%; }
224
245
 
225
246
  /* Network card */
226
247
  .ks-net-card {
@@ -242,19 +263,8 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
242
263
  }
243
264
  .ks-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
244
265
  .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
- }
266
+ .ks-stat-label { font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); }
267
+ .ks-stat-value { font-size: .95rem; color: var(--ks-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
258
268
 
259
269
  /* Link cards */
260
270
  .ks-link-card {
@@ -262,19 +272,13 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
262
272
  display: flex;
263
273
  flex-direction: column;
264
274
  justify-content: space-between;
265
- gap: 0.5rem;
275
+ gap: .5rem;
266
276
  min-height: 100px;
267
277
  }
268
278
  .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
- }
279
+ .ks-link-icon { color: var(--ks-accent); }
280
+ .ks-link-arrow { color: var(--ks-muted); }
281
+ .ks-link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
278
282
  .ks-link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
279
283
  .ks-link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
280
284
 
@@ -283,8 +287,9 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
283
287
  border: 1px solid var(--ks-border);
284
288
  border-radius: 16px;
285
289
  padding: 0;
286
- max-width: 420px;
287
- width: calc(100vw - 2rem);
290
+ width: min(440px, calc(100vw - 2rem));
291
+ max-width: none;
292
+ overflow: hidden;
288
293
  background: var(--ks-surface);
289
294
  color: var(--ks-text);
290
295
  box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
@@ -305,6 +310,12 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
305
310
  .ks-dialog-close:hover { color: var(--ks-heading); }
306
311
  .ks-dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
307
312
  .ks-dialog-body { font-size: .875rem; color: var(--ks-muted); margin: 0 0 1.25rem; line-height: 1.65; }
313
+
314
+ .ks-copy-wrap {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: .5rem;
318
+ }
308
319
  .ks-dialog-addr {
309
320
  display: block;
310
321
  padding: .6em .85em;
@@ -313,13 +324,31 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
313
324
  border: 1px solid var(--ks-border);
314
325
  font-size: .7rem;
315
326
  word-break: break-all;
327
+ overflow-wrap: anywhere;
328
+ white-space: normal;
316
329
  color: var(--ks-text);
317
330
  }
331
+ .ks-copy-btn {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: .3rem;
335
+ align-self: flex-end;
336
+ padding: .35em .75em;
337
+ border-radius: 6px;
338
+ border: 1px solid var(--ks-border);
339
+ background: var(--ks-soft);
340
+ color: var(--ks-muted);
341
+ font-size: .75rem;
342
+ cursor: pointer;
343
+ transition: color .15s, border-color .15s;
344
+ }
345
+ .ks-copy-btn:hover { color: var(--ks-heading); border-color: var(--ks-heading); }
346
+ .ks-copy-btn.copied { color: #4caf50; border-color: #4caf50; }
347
+
318
348
  .ks-dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
319
349
  </style>
320
350
 
321
351
  <style>
322
- /* ::backdrop can't receive scoped attribute — must be global */
323
352
  .ks-dialog::backdrop {
324
353
  background: rgba(0, 0, 0, .5);
325
354
  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,77 @@ 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 -->
94
- <div ref="bento" class="grid">
95
-
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>
108
+ <!-- Gradient + bento grid -->
109
+ <div class="grid-wrap">
110
+ <div ref="bento" class="grid">
111
+
112
+ <!-- Network card: col 1–2, row 1–3 -->
113
+ <div data-shine class="shine net-shine">
114
+ <div class="card net-card">
115
+ <div class="net-top">
116
+ <span class="net-icon">⬡</span>
117
+ <span class="badge" :style="`border-color:${badgeColor};color:${badgeColor}`">{{ stateLabel }}</span>
118
118
  </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>
119
+ <div class="stats">
120
+ <div class="stat">
121
+ <span class="stat-label">Network</span>
122
+ <span class="stat-value">{{ rpc.networkId.value ?? '—' }}</span>
123
+ </div>
124
+ <div class="stat">
125
+ <span class="stat-label">Server version</span>
126
+ <span class="stat-value">{{ rpc.serverVersion.value ?? '—' }}</span>
127
+ </div>
128
+ <div class="stat">
129
+ <span class="stat-label">DAA Score</span>
130
+ <span class="stat-value mono">{{ daaScore }}</span>
131
+ </div>
132
+ <div class="stat">
133
+ <span class="stat-label">Synced</span>
134
+ <span
135
+ class="stat-value"
136
+ :style="`color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : 'var(--ks-text)') : 'var(--ks-muted)'}`"
137
+ >{{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}</span>
138
+ </div>
125
139
  </div>
126
140
  </div>
127
141
  </div>
128
- </div>
129
142
 
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>
143
+ <!-- Link cards: auto-placed into Γ shape -->
144
+ <a
145
+ v-for="link in links"
146
+ :key="link.href"
147
+ data-shine
148
+ class="shine link-shine"
149
+ :href="link.href"
150
+ target="_blank"
151
+ rel="noopener"
152
+ >
153
+ <div class="card link-card">
154
+ <div class="link-top">
155
+ <component :is="link.icon" :size="20" class="link-icon" />
156
+ <ArrowUpRight :size="13" class="link-arrow" />
157
+ </div>
158
+ <div>
159
+ <div class="link-label">{{ link.label }}</div>
160
+ <div class="link-title">{{ link.title }}</div>
161
+ <div class="link-desc">{{ link.desc }}</div>
162
+ </div>
149
163
  </div>
150
- </div>
151
- </a>
164
+ </a>
152
165
 
166
+ </div>
153
167
  </div>
154
168
  </div>
155
169
  </template>
156
170
 
157
171
  <style scoped>
158
172
  /* Root */
159
- .root {
160
- width: 100%;
161
- font-family: Inter, system-ui, -apple-system, sans-serif;
162
- }
173
+ .root { width: 100%; font-family: Inter, system-ui, -apple-system, sans-serif; }
163
174
 
164
175
  /* Header */
165
176
  .header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
@@ -183,10 +194,30 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
183
194
  }
184
195
  .icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
185
196
 
197
+ /* Grid wrap + U-shaped gradient backdrop */
198
+ .grid-wrap { position: relative; }
199
+ .grid-wrap::before {
200
+ content: '';
201
+ position: absolute;
202
+ inset: 0 -20px -28px;
203
+ z-index: 0;
204
+ pointer-events: none;
205
+ background:
206
+ radial-gradient(ellipse 55% 65% at 8% 100%, rgba(73, 197, 163, .38) 0%, transparent 55%),
207
+ radial-gradient(ellipse 55% 65% at 92% 100%, rgba(73, 197, 163, .38) 0%, transparent 55%),
208
+ radial-gradient(ellipse 85% 30% at 50% 100%, rgba(73, 197, 163, .22) 0%, transparent 60%);
209
+ }
210
+
186
211
  /* Grid */
187
- .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; }
212
+ .grid {
213
+ display: grid;
214
+ grid-template-columns: repeat(3, 1fr);
215
+ gap: .75rem;
216
+ position: relative;
217
+ z-index: 1;
218
+ }
188
219
 
189
- /* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
220
+ /* Shine wrapper */
190
221
  .shine {
191
222
  padding: 1px;
192
223
  border-radius: 14px;
@@ -198,8 +229,6 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
198
229
  display: block;
199
230
  text-decoration: none;
200
231
  }
201
-
202
- /* Network card occupies col 1–2, row 1–3 */
203
232
  .net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
204
233
 
205
234
  /* Card base */
@@ -219,8 +248,8 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
219
248
  /* Link cards */
220
249
  .link-card { padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: .5rem; min-height: 100px; }
221
250
  .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); }
251
+ .link-icon { color: var(--ks-accent); }
252
+ .link-arrow { color: var(--ks-muted); }
224
253
  .link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
225
254
  .link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
226
255
  .link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
@@ -230,8 +259,9 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
230
259
  border: 1px solid var(--ks-border);
231
260
  border-radius: 16px;
232
261
  padding: 0;
233
- max-width: 420px;
234
- width: calc(100vw - 2rem);
262
+ width: min(440px, calc(100vw - 2rem));
263
+ max-width: none;
264
+ overflow: hidden;
235
265
  background: var(--ks-surface);
236
266
  color: var(--ks-text);
237
267
  box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
@@ -241,12 +271,41 @@ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
241
271
  .dialog-close:hover { color: var(--ks-heading); }
242
272
  .dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
243
273
  .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); }
274
+
275
+ .copy-wrap { display: flex; flex-direction: column; gap: .5rem; }
276
+ .dialog-addr {
277
+ display: block;
278
+ padding: .6em .85em;
279
+ border-radius: 8px;
280
+ background: var(--ks-soft);
281
+ border: 1px solid var(--ks-border);
282
+ font-size: .7rem;
283
+ word-break: break-all;
284
+ overflow-wrap: anywhere;
285
+ white-space: normal;
286
+ color: var(--ks-text);
287
+ }
288
+ .copy-btn {
289
+ display: inline-flex;
290
+ align-items: center;
291
+ gap: .3rem;
292
+ align-self: flex-end;
293
+ padding: .35em .75em;
294
+ border-radius: 6px;
295
+ border: 1px solid var(--ks-border);
296
+ background: var(--ks-soft);
297
+ color: var(--ks-muted);
298
+ font-size: .75rem;
299
+ cursor: pointer;
300
+ transition: color .15s, border-color .15s;
301
+ }
302
+ .copy-btn:hover { color: var(--ks-heading); border-color: var(--ks-heading); }
303
+ .copy-btn.copied { color: #4caf50; border-color: #4caf50; }
304
+
245
305
  .dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
246
306
  </style>
247
307
 
248
308
  <style>
249
- /* ::backdrop can't receive scoped attribute — must be global */
250
309
  .dialog::backdrop {
251
310
  background: rgba(0, 0, 0, .5);
252
311
  backdrop-filter: blur(4px);