vue-kaspa-cli 0.1.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-kaspa-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Vue 3 or Nuxt app wired to the Kaspa blockchain",
6
6
  "license": "MIT",
@@ -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,10 +20,10 @@ const stateLabel = computed(() => {
19
20
  })
20
21
 
21
22
  const badgeColor = computed(() => {
22
- if (kaspa.wasmStatus.value !== 'ready') return '#9ca3af'
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 '#9ca3af'
26
+ return 'var(--ks-muted)'
26
27
  })
27
28
 
28
29
  const daaScore = computed(() =>
@@ -30,73 +31,297 @@ const daaScore = computed(() =>
30
31
  )
31
32
 
32
33
  const links = [
33
- { label: 'Faucet', title: 'Testnet 10', href: 'https://faucet-tn10.kaspanet.io/' },
34
- { label: 'Faucet', title: 'Testnet 12', href: 'https://faucet-tn12.kaspanet.io/' },
35
- { label: 'Docs', title: 'vue-kaspa', href: 'https://vue-kaspa.vercel.app/' },
36
- { label: 'Explorer', title: 'Testnet 10', href: 'https://tn10.kaspa.stream/' },
37
- { label: 'Explorer', title: 'Testnet 12', href: 'https://tn12.kaspa.stream/' },
38
- { label: 'Explorer', title: 'Mainnet', href: 'https://kaspa.stream/' },
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/' },
39
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))
40
53
  </script>
41
54
 
42
55
  <template>
43
- <div style="width:100%;max-width:480px;display:flex;flex-direction:column;gap:0.75rem;font-family:Inter,system-ui,sans-serif;">
44
-
45
- <!-- Network status card (unchanged) -->
46
- <div style="padding:2rem;border:1px solid #e2e8f0;border-radius:10px;background:#f9f9f9;display:flex;flex-direction:column;gap:1.5rem;">
47
- <header style="display:flex;align-items:center;gap:.75rem;">
48
- <span style="font-size:1.75rem;color:#49c5a3;line-height:1;">⬡</span>
49
- <h1 style="font-size:1.25rem;font-weight:600;color:#1a1a1a;flex:1;margin:0;">vue-kaspa</h1>
50
- <span :style="`font-size:.75rem;font-weight:500;padding:.2em .65em;border-radius:999px;border:1px solid ${badgeColor};color:${badgeColor};white-space:nowrap;`">
51
- {{ stateLabel }}
52
- </span>
53
- </header>
54
-
55
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
56
- <div style="display:flex;flex-direction:column;gap:.25rem;">
57
- <span style="font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">Network</span>
58
- <span style="font-size:.95rem;color:#1a1a1a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ rpc.networkId.value ?? '—' }}</span>
59
- </div>
60
- <div style="display:flex;flex-direction:column;gap:.25rem;">
61
- <span style="font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">Server version</span>
62
- <span style="font-size:.95rem;color:#1a1a1a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ rpc.serverVersion.value ?? '—' }}</span>
63
- </div>
64
- <div style="display:flex;flex-direction:column;gap:.25rem;">
65
- <span style="font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">DAA Score</span>
66
- <span style="font-size:.95rem;color:#1a1a1a;font-family:monospace;">{{ daaScore }}</span>
67
- </div>
68
- <div style="display:flex;flex-direction:column;gap:.25rem;">
69
- <span style="font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">Synced</span>
70
- <span :style="`font-size:.95rem;color:${rpc.isConnected.value ? (rpc.isSynced.value ? '#4caf50' : '#1a1a1a') : '#9ca3af'};`">
71
- {{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}
72
- </span>
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>
91
+ </header>
92
+
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>
73
127
  </div>
74
128
  </div>
75
129
 
76
- <footer style="display:flex;align-items:center;gap:.5rem;font-size:.85rem;border-top:1px solid #e2e8f0;padding-top:1rem;">
77
- <a href="https://vue-kaspa.vercel.app" target="_blank" rel="noopener" style="color:#374151;text-decoration:none;">Docs</a>
78
- <span>·</span>
79
- <a href="https://github.com/furatamasensei/vue-kaspa" target="_blank" rel="noopener" style="color:#374151;text-decoration:none;">GitHub</a>
80
- </footer>
81
- </div>
82
-
83
- <!-- Bento link cards -->
84
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem;">
130
+ <!-- Link cards — auto-placed: first 3 fill col 3 rows 1–3, last 3 fill row 4 -->
85
131
  <a
86
132
  v-for="link in links"
87
133
  :key="link.href"
134
+ data-shine
135
+ class="ks-shine ks-link-shine"
88
136
  :href="link.href"
89
137
  target="_blank"
90
138
  rel="noopener"
91
- style="display:flex;flex-direction:column;justify-content:space-between;gap:0.75rem;padding:1rem;border:1px solid #e2e8f0;border-radius:10px;background:#f9f9f9;text-decoration:none;cursor:pointer;"
92
139
  >
93
- <div style="display:flex;justify-content:space-between;align-items:center;">
94
- <span style="font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;">{{ link.label }}</span>
95
- <span style="font-size:.75rem;color:#9ca3af;">↗</span>
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>
96
150
  </div>
97
- <span style="font-size:.85rem;font-weight:600;color:#1a1a1a;">{{ link.title }}</span>
98
151
  </a>
99
- </div>
100
152
 
153
+ </div>
101
154
  </div>
102
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>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <main style="min-height:100vh;display:flex;align-items:center;justify-content:center;">
2
+ <main style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem 1.5rem;">
3
3
  <ClientOnly>
4
4
  <KaspaStatus />
5
5
  </ClientOnly>
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,11 +19,11 @@ const stateLabel = computed(() => {
17
19
  } as Record<string, string>)[rpc.connectionState.value] ?? rpc.connectionState.value
18
20
  })
19
21
 
20
- const stateClass = computed(() => {
21
- if (kaspa.wasmStatus.value !== 'ready') return 'badge--pending'
22
- if (rpc.connectionState.value === 'connected') return 'badge--ok'
23
- if (rpc.connectionState.value === 'error') return 'badge--error'
24
- return 'badge--pending'
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(() =>
@@ -29,170 +31,224 @@ const daaScore = computed(() =>
29
31
  )
30
32
 
31
33
  const links = [
32
- { label: 'Faucet', title: 'Testnet 10', href: 'https://faucet-tn10.kaspanet.io/' },
33
- { label: 'Faucet', title: 'Testnet 12', href: 'https://faucet-tn12.kaspanet.io/' },
34
- { label: 'Docs', title: 'vue-kaspa', href: 'https://vue-kaspa.vercel.app/' },
35
- { label: 'Explorer', title: 'Testnet 10', href: 'https://tn10.kaspa.stream/' },
36
- { label: 'Explorer', title: 'Testnet 12', href: 'https://tn12.kaspa.stream/' },
37
- { label: 'Explorer', title: 'Mainnet', href: 'https://kaspa.stream/' },
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/' },
38
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))
39
53
  </script>
40
54
 
41
55
  <template>
42
- <div class="bento">
43
-
44
- <!-- Network status card (unchanged) -->
45
- <div class="card">
46
- <header class="card__header">
47
- <span class="logo">⬡</span>
48
- <h1 class="card__title">vue-kaspa</h1>
49
- <span class="badge" :class="stateClass">{{ stateLabel }}</span>
50
- </header>
51
-
52
- <div class="grid">
53
- <div class="stat">
54
- <span class="stat__label">Network</span>
55
- <span class="stat__value">{{ rpc.networkId.value ?? '—' }}</span>
56
- </div>
57
- <div class="stat">
58
- <span class="stat__label">Server version</span>
59
- <span class="stat__value">{{ rpc.serverVersion.value ?? '—' }}</span>
60
- </div>
61
- <div class="stat">
62
- <span class="stat__label">DAA Score</span>
63
- <span class="stat__value mono">{{ daaScore }}</span>
64
- </div>
65
- <div class="stat">
66
- <span class="stat__label">Synced</span>
67
- <span class="stat__value" :class="{ ok: rpc.isSynced.value, muted: !rpc.isConnected.value }">
68
- {{ rpc.isConnected.value ? (rpc.isSynced.value ? 'Yes' : 'Syncing…') : '—' }}
69
- </span>
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>
91
+ </header>
92
+
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>
70
127
  </div>
71
128
  </div>
72
129
 
73
- <footer class="card__footer">
74
- <a href="https://vue-kaspa.vercel.app" target="_blank" rel="noopener">Docs</a>
75
- <span>·</span>
76
- <a href="https://github.com/furatamasensei/vue-kaspa" target="_blank" rel="noopener">GitHub</a>
77
- </footer>
78
- </div>
79
-
80
- <!-- Bento link cards -->
81
- <div class="link-grid">
130
+ <!-- Link cards — auto-placed: first 3 fill col 3 rows 1–3, last 3 fill row 4 -->
82
131
  <a
83
132
  v-for="link in links"
84
133
  :key="link.href"
134
+ data-shine
135
+ class="shine link-shine"
85
136
  :href="link.href"
86
137
  target="_blank"
87
138
  rel="noopener"
88
- class="link-card"
89
139
  >
90
- <div class="link-card__top">
91
- <span class="link-card__label">{{ link.label }}</span>
92
- <span class="link-card__arrow">↗</span>
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>
93
150
  </div>
94
- <span class="link-card__title">{{ link.title }}</span>
95
151
  </a>
96
- </div>
97
152
 
153
+ </div>
98
154
  </div>
99
155
  </template>
100
156
 
101
157
  <style scoped>
102
- .bento {
158
+ /* Root */
159
+ .root {
103
160
  width: 100%;
104
- max-width: 480px;
105
- display: flex;
106
- flex-direction: column;
107
- gap: 0.75rem;
108
- }
109
- .card {
110
- padding: 2rem;
111
- border: 1px solid var(--color-border);
112
- border-radius: 10px;
113
- background: var(--color-background-soft);
114
- display: flex;
115
- flex-direction: column;
116
- gap: 1.5rem;
117
- }
118
- .card__header { display: flex; align-items: center; gap: .75rem; }
119
- .logo { font-size: 1.75rem; color: #49c5a3; line-height: 1; }
120
- .card__title { font-size: 1.25rem; font-weight: 600; color: var(--color-heading); flex: 1; margin: 0; }
121
- .badge {
122
- font-size: .75rem;
123
- font-weight: 500;
124
- padding: .2em .65em;
125
- border-radius: 999px;
126
- border: 1px solid currentColor;
127
- white-space: nowrap;
128
- }
129
- .badge--ok { color: #4caf50; }
130
- .badge--pending { color: var(--color-muted, #888); }
131
- .badge--error { color: #f44336; }
132
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
133
- .stat { display: flex; flex-direction: column; gap: .25rem; }
134
- .stat__label {
135
- font-size: .7rem;
136
- text-transform: uppercase;
137
- letter-spacing: .05em;
138
- color: var(--color-muted, #888);
161
+ font-family: Inter, system-ui, -apple-system, sans-serif;
139
162
  }
140
- .stat__value {
141
- font-size: .95rem;
142
- color: var(--color-heading);
143
- overflow: hidden;
144
- text-overflow: ellipsis;
145
- white-space: nowrap;
146
- }
147
- .stat__value.mono { font-family: monospace; }
148
- .stat__value.ok { color: #4caf50; }
149
- .stat__value.muted { color: var(--color-muted, #888); }
150
- .card__footer {
151
- display: flex;
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;
152
172
  align-items: center;
153
- gap: .5rem;
154
- font-size: .85rem;
155
- border-top: 1px solid var(--color-border);
156
- padding-top: 1rem;
157
- }
158
- .card__footer a { color: var(--color-text); text-decoration: none; }
159
- .card__footer a:hover { color: var(--color-heading); }
160
-
161
- /* Bento link grid */
162
- .link-grid {
163
- display: grid;
164
- grid-template-columns: repeat(3, 1fr);
165
- gap: 0.75rem;
166
- }
167
- .link-card {
168
- display: flex;
169
- flex-direction: column;
170
- justify-content: space-between;
171
- gap: 0.75rem;
172
- padding: 1rem;
173
- border: 1px solid var(--color-border);
174
- border-radius: 10px;
175
- background: var(--color-background-soft);
176
- text-decoration: none;
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);
177
180
  cursor: pointer;
178
- transition: border-color 0.15s;
181
+ text-decoration: none;
182
+ transition: color .15s, background .15s;
179
183
  }
180
- .link-card:hover { border-color: #49c5a3; }
181
- .link-card__top {
182
- display: flex;
183
- justify-content: space-between;
184
- align-items: center;
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;
185
200
  }
186
- .link-card__label {
187
- font-size: .65rem;
188
- text-transform: uppercase;
189
- letter-spacing: .05em;
190
- color: var(--color-muted, #888);
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);
191
238
  }
192
- .link-card__arrow { font-size: .75rem; color: var(--color-muted, #888); }
193
- .link-card__title {
194
- font-size: .85rem;
195
- font-weight: 600;
196
- color: var(--color-heading);
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);
197
253
  }
198
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
- --color-background: #ffffff;
7
- --color-background-soft: #f9f9f9;
8
- --color-border: #e2e8f0;
9
- --color-heading: #1a1a1a;
10
- --color-text: #374151;
11
- --color-muted: #9ca3af;
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
- --color-background: #1a1a1a;
17
- --color-background-soft: #242424;
18
- --color-border: #2d2d2d;
19
- --color-heading: #ffffff;
20
- --color-text: #d1d5db;
21
- --color-muted: #6b7280;
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(--color-background);
32
- color: var(--color-text);
32
+ background: var(--ks-surface);
33
+ color: var(--ks-text);
33
34
  font-family: Inter, system-ui, -apple-system, sans-serif;
34
- font-size: 16px;
35
+ padding: 2rem 1.5rem;
35
36
  }
36
37
 
37
38
  #app {
38
39
  width: 100%;
39
- max-width: 600px;
40
- padding: 1.5rem;
40
+ max-width: 720px;
41
41
  }