vue-kaspa-cli 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/nuxt/app/app.vue +35 -0
- package/templates/nuxt/app/components/KaspaStatus.vue +292 -35
- package/templates/nuxt/app/pages/index.vue +1 -1
- package/templates/nuxt/public/logo.png +0 -0
- package/templates/vue/public/logo.png +0 -0
- package/templates/vue/src/components/KaspaStatus.vue +213 -82
- package/templates/vue/src/style.css +20 -20
package/package.json
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<NuxtPage />
|
|
3
3
|
</template>
|
|
4
|
+
|
|
5
|
+
<style>
|
|
6
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
--ks-surface: #ffffff;
|
|
10
|
+
--ks-soft: #f4f4f5;
|
|
11
|
+
--ks-border: #e4e4e7;
|
|
12
|
+
--ks-heading: #18181b;
|
|
13
|
+
--ks-text: #3f3f46;
|
|
14
|
+
--ks-muted: #a1a1aa;
|
|
15
|
+
--ks-accent: #49c5a3;
|
|
16
|
+
--ks-shine: rgba(73, 197, 163, .55);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
:root {
|
|
21
|
+
--ks-surface: #09090b;
|
|
22
|
+
--ks-soft: #18181b;
|
|
23
|
+
--ks-border: #27272a;
|
|
24
|
+
--ks-heading: #fafafa;
|
|
25
|
+
--ks-text: #a1a1aa;
|
|
26
|
+
--ks-muted: #52525b;
|
|
27
|
+
--ks-shine: rgba(73, 197, 163, .40);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body {
|
|
32
|
+
margin: 0;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
background: var(--ks-surface);
|
|
35
|
+
color: var(--ks-text);
|
|
36
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
2
3
|
// useKaspa, useRpc, and computed are auto-imported by Nuxt.
|
|
3
|
-
// WASM init and RPC connection are handled automatically by the vue-kaspa Nuxt module
|
|
4
|
-
// when autoConnect: true (the default). No manual connect call is needed here.
|
|
5
4
|
|
|
6
5
|
const kaspa = useKaspa()
|
|
7
6
|
const rpc = useRpc()
|
|
7
|
+
const bento = ref<HTMLElement | null>(null)
|
|
8
|
+
const donateDialog = ref<HTMLDialogElement | null>(null)
|
|
8
9
|
|
|
9
10
|
const stateLabel = computed(() => {
|
|
10
11
|
if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
|
|
@@ -19,52 +20,308 @@ const stateLabel = computed(() => {
|
|
|
19
20
|
})
|
|
20
21
|
|
|
21
22
|
const badgeColor = computed(() => {
|
|
22
|
-
if (kaspa.wasmStatus.value !== 'ready') return '
|
|
23
|
+
if (kaspa.wasmStatus.value !== 'ready') return 'var(--ks-muted)'
|
|
23
24
|
if (rpc.connectionState.value === 'connected') return '#4caf50'
|
|
24
25
|
if (rpc.connectionState.value === 'error') return '#f44336'
|
|
25
|
-
return '
|
|
26
|
+
return 'var(--ks-muted)'
|
|
26
27
|
})
|
|
27
28
|
|
|
28
29
|
const daaScore = computed(() =>
|
|
29
30
|
rpc.virtualDaaScore.value > 0n ? rpc.virtualDaaScore.value.toLocaleString() : '—'
|
|
30
31
|
)
|
|
32
|
+
|
|
33
|
+
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/' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function onMouseMove(e: MouseEvent) {
|
|
43
|
+
const cards = bento.value?.querySelectorAll<HTMLElement>('[data-shine]') ?? []
|
|
44
|
+
for (const card of cards) {
|
|
45
|
+
const r = card.getBoundingClientRect()
|
|
46
|
+
card.style.setProperty('--x', `${e.clientX - r.left}px`)
|
|
47
|
+
card.style.setProperty('--y', `${e.clientY - r.top}px`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onMounted(() => window.addEventListener('mousemove', onMouseMove))
|
|
52
|
+
onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
|
|
31
53
|
</script>
|
|
32
54
|
|
|
33
55
|
<template>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
</
|
|
56
|
+
<!-- Donation dialog -->
|
|
57
|
+
<dialog ref="donateDialog" class="ks-dialog" @click.self="donateDialog?.close()">
|
|
58
|
+
<div class="ks-dialog-inner">
|
|
59
|
+
<button class="ks-dialog-close" @click="donateDialog?.close()">✕</button>
|
|
60
|
+
<p class="ks-dialog-title">Support vue-kaspa ❤️</p>
|
|
61
|
+
<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>
|
|
63
|
+
<p class="ks-dialog-thanks">Thank you for your support 🙏</p>
|
|
64
|
+
</div>
|
|
65
|
+
</dialog>
|
|
66
|
+
|
|
67
|
+
<div class="ks-root">
|
|
68
|
+
|
|
69
|
+
<!-- Header -->
|
|
70
|
+
<header class="ks-header">
|
|
71
|
+
<img src="/logo.png" alt="vue-kaspa" class="ks-header-logo" />
|
|
72
|
+
<span class="ks-header-brand">vue-kaspa</span>
|
|
73
|
+
<nav class="ks-header-nav">
|
|
74
|
+
<a
|
|
75
|
+
href="https://github.com/furatamasensei/vue-kaspa"
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener"
|
|
78
|
+
class="ks-icon-btn"
|
|
79
|
+
title="GitHub"
|
|
80
|
+
>
|
|
81
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
82
|
+
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.741 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
|
|
83
|
+
</svg>
|
|
84
|
+
</a>
|
|
85
|
+
<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>
|
|
89
|
+
</button>
|
|
90
|
+
</nav>
|
|
41
91
|
</header>
|
|
42
92
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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">
|
|
95
|
+
|
|
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>
|
|
121
|
+
<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>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
61
128
|
</div>
|
|
62
|
-
</div>
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
<a
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</a>
|
|
152
|
+
|
|
153
|
+
</div>
|
|
69
154
|
</div>
|
|
70
155
|
</template>
|
|
156
|
+
|
|
157
|
+
<style scoped>
|
|
158
|
+
/* Root */
|
|
159
|
+
.ks-root {
|
|
160
|
+
width: 100%;
|
|
161
|
+
max-width: 720px;
|
|
162
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Header */
|
|
166
|
+
.ks-header {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 0.6rem;
|
|
170
|
+
margin-bottom: 0.75rem;
|
|
171
|
+
}
|
|
172
|
+
.ks-header-logo { width: 28px; height: 28px; object-fit: contain; }
|
|
173
|
+
.ks-header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
|
|
174
|
+
.ks-header-nav { display: flex; gap: 0.15rem; }
|
|
175
|
+
|
|
176
|
+
.ks-icon-btn {
|
|
177
|
+
display: inline-flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
width: 34px;
|
|
181
|
+
height: 34px;
|
|
182
|
+
border-radius: 8px;
|
|
183
|
+
border: none;
|
|
184
|
+
background: transparent;
|
|
185
|
+
color: var(--ks-muted);
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
text-decoration: none;
|
|
188
|
+
transition: color 0.15s, background 0.15s;
|
|
189
|
+
}
|
|
190
|
+
.ks-icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
|
|
191
|
+
|
|
192
|
+
/* Grid */
|
|
193
|
+
.ks-grid {
|
|
194
|
+
display: grid;
|
|
195
|
+
grid-template-columns: repeat(3, 1fr);
|
|
196
|
+
gap: 0.75rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
|
|
200
|
+
.ks-shine {
|
|
201
|
+
padding: 1px;
|
|
202
|
+
border-radius: 14px;
|
|
203
|
+
background: radial-gradient(
|
|
204
|
+
350px circle at var(--x, -9999px) var(--y, -9999px),
|
|
205
|
+
var(--ks-shine),
|
|
206
|
+
var(--ks-border) 80%
|
|
207
|
+
);
|
|
208
|
+
display: block;
|
|
209
|
+
text-decoration: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
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
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Network card */
|
|
226
|
+
.ks-net-card {
|
|
227
|
+
padding: 1.75rem;
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 1.5rem;
|
|
231
|
+
min-height: 320px;
|
|
232
|
+
}
|
|
233
|
+
.ks-net-top { display: flex; align-items: center; justify-content: space-between; }
|
|
234
|
+
.ks-net-icon { font-size: 2.25rem; color: var(--ks-accent); line-height: 1; }
|
|
235
|
+
.ks-badge {
|
|
236
|
+
font-size: .7rem;
|
|
237
|
+
font-weight: 500;
|
|
238
|
+
padding: .2em .6em;
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
border: 1px solid;
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
}
|
|
243
|
+
.ks-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
|
|
244
|
+
.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
|
+
}
|
|
258
|
+
|
|
259
|
+
/* Link cards */
|
|
260
|
+
.ks-link-card {
|
|
261
|
+
padding: 1rem;
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
justify-content: space-between;
|
|
265
|
+
gap: 0.5rem;
|
|
266
|
+
min-height: 100px;
|
|
267
|
+
}
|
|
268
|
+
.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
|
+
}
|
|
278
|
+
.ks-link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
|
|
279
|
+
.ks-link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
|
|
280
|
+
|
|
281
|
+
/* Dialog */
|
|
282
|
+
.ks-dialog {
|
|
283
|
+
border: 1px solid var(--ks-border);
|
|
284
|
+
border-radius: 16px;
|
|
285
|
+
padding: 0;
|
|
286
|
+
max-width: 420px;
|
|
287
|
+
width: calc(100vw - 2rem);
|
|
288
|
+
background: var(--ks-surface);
|
|
289
|
+
color: var(--ks-text);
|
|
290
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
|
|
291
|
+
}
|
|
292
|
+
.ks-dialog-inner { padding: 2rem; position: relative; }
|
|
293
|
+
.ks-dialog-close {
|
|
294
|
+
position: absolute;
|
|
295
|
+
top: 1rem;
|
|
296
|
+
right: 1rem;
|
|
297
|
+
background: none;
|
|
298
|
+
border: none;
|
|
299
|
+
font-size: .9rem;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
color: var(--ks-muted);
|
|
302
|
+
padding: .25rem;
|
|
303
|
+
line-height: 1;
|
|
304
|
+
}
|
|
305
|
+
.ks-dialog-close:hover { color: var(--ks-heading); }
|
|
306
|
+
.ks-dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
|
|
307
|
+
.ks-dialog-body { font-size: .875rem; color: var(--ks-muted); margin: 0 0 1.25rem; line-height: 1.65; }
|
|
308
|
+
.ks-dialog-addr {
|
|
309
|
+
display: block;
|
|
310
|
+
padding: .6em .85em;
|
|
311
|
+
border-radius: 8px;
|
|
312
|
+
background: var(--ks-soft);
|
|
313
|
+
border: 1px solid var(--ks-border);
|
|
314
|
+
font-size: .7rem;
|
|
315
|
+
word-break: break-all;
|
|
316
|
+
color: var(--ks-text);
|
|
317
|
+
}
|
|
318
|
+
.ks-dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
|
|
319
|
+
</style>
|
|
320
|
+
|
|
321
|
+
<style>
|
|
322
|
+
/* ::backdrop can't receive scoped attribute — must be global */
|
|
323
|
+
.ks-dialog::backdrop {
|
|
324
|
+
background: rgba(0, 0, 0, .5);
|
|
325
|
+
backdrop-filter: blur(4px);
|
|
326
|
+
}
|
|
327
|
+
</style>
|
|
Binary file
|
|
Binary file
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useKaspa, useRpc } from 'vue-kaspa'
|
|
4
4
|
|
|
5
5
|
const kaspa = useKaspa()
|
|
6
6
|
const rpc = useRpc()
|
|
7
|
+
const bento = ref<HTMLElement | null>(null)
|
|
8
|
+
const donateDialog = ref<HTMLDialogElement | null>(null)
|
|
7
9
|
|
|
8
10
|
const stateLabel = computed(() => {
|
|
9
11
|
if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
|
|
@@ -17,107 +19,236 @@ const stateLabel = computed(() => {
|
|
|
17
19
|
} as Record<string, string>)[rpc.connectionState.value] ?? rpc.connectionState.value
|
|
18
20
|
})
|
|
19
21
|
|
|
20
|
-
const
|
|
21
|
-
if (kaspa.wasmStatus.value !== 'ready') return '
|
|
22
|
-
if (rpc.connectionState.value === 'connected') return '
|
|
23
|
-
if (rpc.connectionState.value === 'error') return '
|
|
24
|
-
return '
|
|
22
|
+
const badgeColor = computed(() => {
|
|
23
|
+
if (kaspa.wasmStatus.value !== 'ready') return 'var(--ks-muted)'
|
|
24
|
+
if (rpc.connectionState.value === 'connected') return '#4caf50'
|
|
25
|
+
if (rpc.connectionState.value === 'error') return '#f44336'
|
|
26
|
+
return 'var(--ks-muted)'
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
const daaScore = computed(() =>
|
|
28
30
|
rpc.virtualDaaScore.value > 0n ? rpc.virtualDaaScore.value.toLocaleString() : '—'
|
|
29
31
|
)
|
|
32
|
+
|
|
33
|
+
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/' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
function onMouseMove(e: MouseEvent) {
|
|
43
|
+
const cards = bento.value?.querySelectorAll<HTMLElement>('[data-shine]') ?? []
|
|
44
|
+
for (const card of cards) {
|
|
45
|
+
const r = card.getBoundingClientRect()
|
|
46
|
+
card.style.setProperty('--x', `${e.clientX - r.left}px`)
|
|
47
|
+
card.style.setProperty('--y', `${e.clientY - r.top}px`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onMounted(() => window.addEventListener('mousemove', onMouseMove))
|
|
52
|
+
onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
|
|
30
53
|
</script>
|
|
31
54
|
|
|
32
55
|
<template>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
<
|
|
56
|
+
<!-- Donation dialog -->
|
|
57
|
+
<dialog ref="donateDialog" class="dialog" @click.self="donateDialog?.close()">
|
|
58
|
+
<div class="dialog-inner">
|
|
59
|
+
<button class="dialog-close" @click="donateDialog?.close()">✕</button>
|
|
60
|
+
<p class="dialog-title">Support vue-kaspa ❤️</p>
|
|
61
|
+
<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>
|
|
63
|
+
<p class="dialog-thanks">Thank you for your support 🙏</p>
|
|
64
|
+
</div>
|
|
65
|
+
</dialog>
|
|
66
|
+
|
|
67
|
+
<div class="root">
|
|
68
|
+
|
|
69
|
+
<!-- Header -->
|
|
70
|
+
<header class="header">
|
|
71
|
+
<img src="/logo.png" alt="vue-kaspa" class="header-logo" />
|
|
72
|
+
<span class="header-brand">vue-kaspa</span>
|
|
73
|
+
<nav class="header-nav">
|
|
74
|
+
<a
|
|
75
|
+
href="https://github.com/furatamasensei/vue-kaspa"
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener"
|
|
78
|
+
class="icon-btn"
|
|
79
|
+
title="GitHub"
|
|
80
|
+
>
|
|
81
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
82
|
+
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.741 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
|
|
83
|
+
</svg>
|
|
84
|
+
</a>
|
|
85
|
+
<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>
|
|
89
|
+
</button>
|
|
90
|
+
</nav>
|
|
38
91
|
</header>
|
|
39
92
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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>
|
|
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>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
58
128
|
</div>
|
|
59
|
-
</div>
|
|
60
129
|
|
|
61
|
-
|
|
62
|
-
<a
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</a>
|
|
152
|
+
|
|
153
|
+
</div>
|
|
66
154
|
</div>
|
|
67
155
|
</template>
|
|
68
156
|
|
|
69
157
|
<style scoped>
|
|
70
|
-
|
|
158
|
+
/* Root */
|
|
159
|
+
.root {
|
|
71
160
|
width: 100%;
|
|
72
|
-
|
|
73
|
-
padding: 2rem;
|
|
74
|
-
border: 1px solid var(--color-border);
|
|
75
|
-
border-radius: 10px;
|
|
76
|
-
background: var(--color-background-soft);
|
|
77
|
-
display: flex;
|
|
78
|
-
flex-direction: column;
|
|
79
|
-
gap: 1.5rem;
|
|
161
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
80
162
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
163
|
+
|
|
164
|
+
/* Header */
|
|
165
|
+
.header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
|
166
|
+
.header-logo { width: 28px; height: 28px; object-fit: contain; }
|
|
167
|
+
.header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
|
|
168
|
+
.header-nav { display: flex; gap: .15rem; }
|
|
169
|
+
|
|
170
|
+
.icon-btn {
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
width: 34px;
|
|
175
|
+
height: 34px;
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
border: none;
|
|
178
|
+
background: transparent;
|
|
179
|
+
color: var(--ks-muted);
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
text-decoration: none;
|
|
182
|
+
transition: color .15s, background .15s;
|
|
91
183
|
}
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.grid { display: grid; grid-template-columns:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
184
|
+
.icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
|
|
185
|
+
|
|
186
|
+
/* Grid */
|
|
187
|
+
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; }
|
|
188
|
+
|
|
189
|
+
/* Shine wrapper — 1px padding reveals radial gradient as a glowing border */
|
|
190
|
+
.shine {
|
|
191
|
+
padding: 1px;
|
|
192
|
+
border-radius: 14px;
|
|
193
|
+
background: radial-gradient(
|
|
194
|
+
350px circle at var(--x, -9999px) var(--y, -9999px),
|
|
195
|
+
var(--ks-shine),
|
|
196
|
+
var(--ks-border) 80%
|
|
197
|
+
);
|
|
198
|
+
display: block;
|
|
199
|
+
text-decoration: none;
|
|
102
200
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
201
|
+
|
|
202
|
+
/* Network card occupies col 1–2, row 1–3 */
|
|
203
|
+
.net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
|
|
204
|
+
|
|
205
|
+
/* Card base */
|
|
206
|
+
.card { border-radius: 13px; background: var(--ks-soft); height: 100%; }
|
|
207
|
+
|
|
208
|
+
/* Network card */
|
|
209
|
+
.net-card { padding: 1.75rem; display: flex; flex-direction: column; gap: 1.5rem; min-height: 320px; }
|
|
210
|
+
.net-top { display: flex; align-items: center; justify-content: space-between; }
|
|
211
|
+
.net-icon { font-size: 2.25rem; color: var(--ks-accent); line-height: 1; }
|
|
212
|
+
.badge { font-size: .7rem; font-weight: 500; padding: .2em .6em; border-radius: 999px; border: 1px solid; white-space: nowrap; }
|
|
213
|
+
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
|
|
214
|
+
.stat { display: flex; flex-direction: column; gap: .25rem; }
|
|
215
|
+
.stat-label { font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); }
|
|
216
|
+
.stat-value { font-size: .95rem; color: var(--ks-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
217
|
+
.mono { font-family: monospace; }
|
|
218
|
+
|
|
219
|
+
/* Link cards */
|
|
220
|
+
.link-card { padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: .5rem; min-height: 100px; }
|
|
221
|
+
.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); }
|
|
224
|
+
.link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
|
|
225
|
+
.link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
|
|
226
|
+
.link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
|
|
227
|
+
|
|
228
|
+
/* Dialog */
|
|
229
|
+
.dialog {
|
|
230
|
+
border: 1px solid var(--ks-border);
|
|
231
|
+
border-radius: 16px;
|
|
232
|
+
padding: 0;
|
|
233
|
+
max-width: 420px;
|
|
234
|
+
width: calc(100vw - 2rem);
|
|
235
|
+
background: var(--ks-surface);
|
|
236
|
+
color: var(--ks-text);
|
|
237
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
|
|
109
238
|
}
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
239
|
+
.dialog-inner { padding: 2rem; position: relative; }
|
|
240
|
+
.dialog-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: .9rem; cursor: pointer; color: var(--ks-muted); padding: .25rem; line-height: 1; }
|
|
241
|
+
.dialog-close:hover { color: var(--ks-heading); }
|
|
242
|
+
.dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
|
|
243
|
+
.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); }
|
|
245
|
+
.dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
|
|
246
|
+
</style>
|
|
247
|
+
|
|
248
|
+
<style>
|
|
249
|
+
/* ::backdrop can't receive scoped attribute — must be global */
|
|
250
|
+
.dialog::backdrop {
|
|
251
|
+
background: rgba(0, 0, 0, .5);
|
|
252
|
+
backdrop-filter: blur(4px);
|
|
120
253
|
}
|
|
121
|
-
.card__footer a { color: var(--color-text); text-decoration: none; }
|
|
122
|
-
.card__footer a:hover { color: var(--color-heading); }
|
|
123
254
|
</style>
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
*, *::before, *::after {
|
|
2
|
-
box-sizing: border-box;
|
|
3
|
-
}
|
|
1
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
4
2
|
|
|
5
3
|
:root {
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
--
|
|
10
|
-
--
|
|
11
|
-
--
|
|
4
|
+
--ks-surface: #ffffff;
|
|
5
|
+
--ks-soft: #f4f4f5;
|
|
6
|
+
--ks-border: #e4e4e7;
|
|
7
|
+
--ks-heading: #18181b;
|
|
8
|
+
--ks-text: #3f3f46;
|
|
9
|
+
--ks-muted: #a1a1aa;
|
|
10
|
+
--ks-accent: #49c5a3;
|
|
11
|
+
--ks-shine: rgba(73, 197, 163, .55);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
@media (prefers-color-scheme: dark) {
|
|
15
15
|
:root {
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
16
|
+
--ks-surface: #09090b;
|
|
17
|
+
--ks-soft: #18181b;
|
|
18
|
+
--ks-border: #27272a;
|
|
19
|
+
--ks-heading: #fafafa;
|
|
20
|
+
--ks-text: #a1a1aa;
|
|
21
|
+
--ks-muted: #52525b;
|
|
22
|
+
--ks-shine: rgba(73, 197, 163, .40);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -28,14 +29,13 @@ body {
|
|
|
28
29
|
display: flex;
|
|
29
30
|
align-items: center;
|
|
30
31
|
justify-content: center;
|
|
31
|
-
background: var(--
|
|
32
|
-
color: var(--
|
|
32
|
+
background: var(--ks-surface);
|
|
33
|
+
color: var(--ks-text);
|
|
33
34
|
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
34
|
-
|
|
35
|
+
padding: 2rem 1.5rem;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
#app {
|
|
38
39
|
width: 100%;
|
|
39
|
-
max-width:
|
|
40
|
-
padding: 1.5rem;
|
|
40
|
+
max-width: 720px;
|
|
41
41
|
}
|