vue-kaspa-cli 0.1.8 → 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.8",
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,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,21 @@
1
1
  <script setup lang="ts">
2
- // 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.
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
+ import { Droplet, BookOpen, Search, ArrowUpRight, Heart, Copy, Check } from 'lucide-vue-next'
4
+ // useKaspa and useRpc are auto-imported by Nuxt.
5
5
 
6
6
  const kaspa = useKaspa()
7
7
  const rpc = useRpc()
8
+ const bento = ref<HTMLElement | null>(null)
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
+ }
8
19
 
9
20
  const stateLabel = computed(() => {
10
21
  if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
@@ -19,10 +30,10 @@ const stateLabel = computed(() => {
19
30
  })
20
31
 
21
32
  const badgeColor = computed(() => {
22
- if (kaspa.wasmStatus.value !== 'ready') return '#9ca3af'
33
+ if (kaspa.wasmStatus.value !== 'ready') return 'var(--ks-muted)'
23
34
  if (rpc.connectionState.value === 'connected') return '#4caf50'
24
35
  if (rpc.connectionState.value === 'error') return '#f44336'
25
- return '#9ca3af'
36
+ return 'var(--ks-muted)'
26
37
  })
27
38
 
28
39
  const daaScore = computed(() =>
@@ -30,73 +41,316 @@ const daaScore = computed(() =>
30
41
  )
31
42
 
32
43
  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/' },
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/' },
39
50
  ]
51
+
52
+ function onMouseMove(e: MouseEvent) {
53
+ const cards = bento.value?.querySelectorAll<HTMLElement>('[data-shine]') ?? []
54
+ for (const card of cards) {
55
+ const r = card.getBoundingClientRect()
56
+ card.style.setProperty('--x', `${e.clientX - r.left}px`)
57
+ card.style.setProperty('--y', `${e.clientY - r.top}px`)
58
+ }
59
+ }
60
+
61
+ onMounted(() => window.addEventListener('mousemove', onMouseMove))
62
+ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
40
63
  </script>
41
64
 
42
65
  <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>
73
- </div>
66
+ <!-- Donation dialog -->
67
+ <dialog ref="donateDialog" class="ks-dialog" @click.self="donateDialog?.close()">
68
+ <div class="ks-dialog-inner">
69
+ <button class="ks-dialog-close" @click="donateDialog?.close()">✕</button>
70
+ <p class="ks-dialog-title">Support vue-kaspa ❤️</p>
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>
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>
74
79
  </div>
75
-
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>
80
+ <p class="ks-dialog-thanks">Thank you for your support 🙏</p>
81
81
  </div>
82
+ </dialog>
83
+
84
+ <div class="ks-root">
85
+
86
+ <!-- Header -->
87
+ <header class="ks-header">
88
+ <img src="/logo.png" alt="vue-kaspa" class="ks-header-logo" />
89
+ <span class="ks-header-brand">vue-kaspa</span>
90
+ <nav class="ks-header-nav">
91
+ <a
92
+ href="https://github.com/furatamasensei/vue-kaspa"
93
+ target="_blank"
94
+ rel="noopener"
95
+ class="ks-icon-btn"
96
+ title="GitHub"
97
+ >
98
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
99
+ <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"/>
100
+ </svg>
101
+ </a>
102
+ <button class="ks-icon-btn" title="Support this project" @click="donateDialog?.showModal()">
103
+ <Heart :size="18" />
104
+ </button>
105
+ </nav>
106
+ </header>
107
+
108
+ <!-- Gradient + bento grid -->
109
+ <div class="ks-grid-wrap">
110
+ <div ref="bento" class="ks-grid">
82
111
 
83
- <!-- Bento link cards -->
84
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem;">
85
- <a
86
- v-for="link in links"
87
- :key="link.href"
88
- :href="link.href"
89
- target="_blank"
90
- 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
- >
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>
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>
117
+ <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>
142
+ </div>
143
+ </div>
96
144
  </div>
97
- <span style="font-size:.85rem;font-weight:600;color:#1a1a1a;">{{ link.title }}</span>
98
- </a>
99
- </div>
100
145
 
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>
166
+ </div>
167
+ </a>
168
+
169
+ </div>
170
+ </div>
101
171
  </div>
102
172
  </template>
173
+
174
+ <style scoped>
175
+ /* Root */
176
+ .ks-root {
177
+ width: 100%;
178
+ max-width: 720px;
179
+ font-family: Inter, system-ui, -apple-system, sans-serif;
180
+ }
181
+
182
+ /* Header */
183
+ .ks-header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
184
+ .ks-header-logo { width: 28px; height: 28px; object-fit: contain; }
185
+ .ks-header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
186
+ .ks-header-nav { display: flex; gap: .15rem; }
187
+
188
+ .ks-icon-btn {
189
+ display: inline-flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ width: 34px;
193
+ height: 34px;
194
+ border-radius: 8px;
195
+ border: none;
196
+ background: transparent;
197
+ color: var(--ks-muted);
198
+ cursor: pointer;
199
+ text-decoration: none;
200
+ transition: color .15s, background .15s;
201
+ }
202
+ .ks-icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
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
+
220
+ /* Grid */
221
+ .ks-grid {
222
+ display: grid;
223
+ grid-template-columns: repeat(3, 1fr);
224
+ gap: .75rem;
225
+ position: relative;
226
+ z-index: 1;
227
+ }
228
+
229
+ /* Shine wrapper */
230
+ .ks-shine {
231
+ padding: 1px;
232
+ border-radius: 14px;
233
+ background: radial-gradient(
234
+ 350px circle at var(--x, -9999px) var(--y, -9999px),
235
+ var(--ks-shine),
236
+ var(--ks-border) 80%
237
+ );
238
+ display: block;
239
+ text-decoration: none;
240
+ }
241
+ .ks-net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
242
+
243
+ /* Card base */
244
+ .ks-card { border-radius: 13px; background: var(--ks-soft); height: 100%; }
245
+
246
+ /* Network card */
247
+ .ks-net-card {
248
+ padding: 1.75rem;
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 1.5rem;
252
+ min-height: 320px;
253
+ }
254
+ .ks-net-top { display: flex; align-items: center; justify-content: space-between; }
255
+ .ks-net-icon { font-size: 2.25rem; color: var(--ks-accent); line-height: 1; }
256
+ .ks-badge {
257
+ font-size: .7rem;
258
+ font-weight: 500;
259
+ padding: .2em .6em;
260
+ border-radius: 999px;
261
+ border: 1px solid;
262
+ white-space: nowrap;
263
+ }
264
+ .ks-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
265
+ .ks-stat { display: flex; flex-direction: column; gap: .25rem; }
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; }
268
+
269
+ /* Link cards */
270
+ .ks-link-card {
271
+ padding: 1rem;
272
+ display: flex;
273
+ flex-direction: column;
274
+ justify-content: space-between;
275
+ gap: .5rem;
276
+ min-height: 100px;
277
+ }
278
+ .ks-link-top { display: flex; justify-content: space-between; align-items: flex-start; }
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; }
282
+ .ks-link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
283
+ .ks-link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
284
+
285
+ /* Dialog */
286
+ .ks-dialog {
287
+ border: 1px solid var(--ks-border);
288
+ border-radius: 16px;
289
+ padding: 0;
290
+ width: min(440px, calc(100vw - 2rem));
291
+ max-width: none;
292
+ overflow: hidden;
293
+ background: var(--ks-surface);
294
+ color: var(--ks-text);
295
+ box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
296
+ }
297
+ .ks-dialog-inner { padding: 2rem; position: relative; }
298
+ .ks-dialog-close {
299
+ position: absolute;
300
+ top: 1rem;
301
+ right: 1rem;
302
+ background: none;
303
+ border: none;
304
+ font-size: .9rem;
305
+ cursor: pointer;
306
+ color: var(--ks-muted);
307
+ padding: .25rem;
308
+ line-height: 1;
309
+ }
310
+ .ks-dialog-close:hover { color: var(--ks-heading); }
311
+ .ks-dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
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
+ }
319
+ .ks-dialog-addr {
320
+ display: block;
321
+ padding: .6em .85em;
322
+ border-radius: 8px;
323
+ background: var(--ks-soft);
324
+ border: 1px solid var(--ks-border);
325
+ font-size: .7rem;
326
+ word-break: break-all;
327
+ overflow-wrap: anywhere;
328
+ white-space: normal;
329
+ color: var(--ks-text);
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
+
348
+ .ks-dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
349
+ </style>
350
+
351
+ <style>
352
+ .ks-dialog::backdrop {
353
+ background: rgba(0, 0, 0, .5);
354
+ backdrop-filter: blur(4px);
355
+ }
356
+ </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
@@ -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
  },
Binary file
@@ -1,9 +1,21 @@
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
+ import { Droplet, BookOpen, Search, ArrowUpRight, Heart, Copy, Check } from 'lucide-vue-next'
4
5
 
5
6
  const kaspa = useKaspa()
6
7
  const rpc = useRpc()
8
+ const bento = ref<HTMLElement | null>(null)
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
+ }
7
19
 
8
20
  const stateLabel = computed(() => {
9
21
  if (kaspa.wasmStatus.value === 'loading') return 'Loading WASM…'
@@ -17,11 +29,11 @@ const stateLabel = computed(() => {
17
29
  } as Record<string, string>)[rpc.connectionState.value] ?? rpc.connectionState.value
18
30
  })
19
31
 
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'
32
+ const badgeColor = computed(() => {
33
+ if (kaspa.wasmStatus.value !== 'ready') return 'var(--ks-muted)'
34
+ if (rpc.connectionState.value === 'connected') return '#4caf50'
35
+ if (rpc.connectionState.value === 'error') return '#f44336'
36
+ return 'var(--ks-muted)'
25
37
  })
26
38
 
27
39
  const daaScore = computed(() =>
@@ -29,170 +41,273 @@ const daaScore = computed(() =>
29
41
  )
30
42
 
31
43
  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/' },
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/' },
38
50
  ]
51
+
52
+ function onMouseMove(e: MouseEvent) {
53
+ const cards = bento.value?.querySelectorAll<HTMLElement>('[data-shine]') ?? []
54
+ for (const card of cards) {
55
+ const r = card.getBoundingClientRect()
56
+ card.style.setProperty('--x', `${e.clientX - r.left}px`)
57
+ card.style.setProperty('--y', `${e.clientY - r.top}px`)
58
+ }
59
+ }
60
+
61
+ onMounted(() => window.addEventListener('mousemove', onMouseMove))
62
+ onUnmounted(() => window.removeEventListener('mousemove', onMouseMove))
39
63
  </script>
40
64
 
41
65
  <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>
70
- </div>
66
+ <!-- Donation dialog -->
67
+ <dialog ref="donateDialog" class="dialog" @click.self="donateDialog?.close()">
68
+ <div class="dialog-inner">
69
+ <button class="dialog-close" @click="donateDialog?.close()">✕</button>
70
+ <p class="dialog-title">Support vue-kaspa ❤️</p>
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>
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>
71
79
  </div>
72
-
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>
80
+ <p class="dialog-thanks">Thank you for your support 🙏</p>
78
81
  </div>
82
+ </dialog>
83
+
84
+ <div class="root">
85
+
86
+ <!-- Header -->
87
+ <header class="header">
88
+ <img src="/logo.png" alt="vue-kaspa" class="header-logo" />
89
+ <span class="header-brand">vue-kaspa</span>
90
+ <nav class="header-nav">
91
+ <a
92
+ href="https://github.com/furatamasensei/vue-kaspa"
93
+ target="_blank"
94
+ rel="noopener"
95
+ class="icon-btn"
96
+ title="GitHub"
97
+ >
98
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
99
+ <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"/>
100
+ </svg>
101
+ </a>
102
+ <button class="icon-btn" title="Support this project" @click="donateDialog?.showModal()">
103
+ <Heart :size="18" />
104
+ </button>
105
+ </nav>
106
+ </header>
79
107
 
80
- <!-- Bento link cards -->
81
- <div class="link-grid">
82
- <a
83
- v-for="link in links"
84
- :key="link.href"
85
- :href="link.href"
86
- target="_blank"
87
- rel="noopener"
88
- class="link-card"
89
- >
90
- <div class="link-card__top">
91
- <span class="link-card__label">{{ link.label }}</span>
92
- <span class="link-card__arrow">↗</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
+ </div>
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>
139
+ </div>
140
+ </div>
93
141
  </div>
94
- <span class="link-card__title">{{ link.title }}</span>
95
- </a>
96
- </div>
97
142
 
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>
163
+ </div>
164
+ </a>
165
+
166
+ </div>
167
+ </div>
98
168
  </div>
99
169
  </template>
100
170
 
101
171
  <style scoped>
102
- .bento {
103
- 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);
139
- }
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;
172
+ /* Root */
173
+ .root { width: 100%; font-family: Inter, system-ui, -apple-system, sans-serif; }
174
+
175
+ /* Header */
176
+ .header { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
177
+ .header-logo { width: 28px; height: 28px; object-fit: contain; }
178
+ .header-brand { font-size: 1rem; font-weight: 700; color: var(--ks-heading); flex: 1; }
179
+ .header-nav { display: flex; gap: .15rem; }
180
+
181
+ .icon-btn {
182
+ display: inline-flex;
152
183
  align-items: center;
153
- gap: .5rem;
154
- font-size: .85rem;
155
- border-top: 1px solid var(--color-border);
156
- padding-top: 1rem;
184
+ justify-content: center;
185
+ width: 34px;
186
+ height: 34px;
187
+ border-radius: 8px;
188
+ border: none;
189
+ background: transparent;
190
+ color: var(--ks-muted);
191
+ cursor: pointer;
192
+ text-decoration: none;
193
+ transition: color .15s, background .15s;
194
+ }
195
+ .icon-btn:hover { color: var(--ks-heading); background: var(--ks-border); }
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%);
157
209
  }
158
- .card__footer a { color: var(--color-text); text-decoration: none; }
159
- .card__footer a:hover { color: var(--color-heading); }
160
210
 
161
- /* Bento link grid */
162
- .link-grid {
211
+ /* Grid */
212
+ .grid {
163
213
  display: grid;
164
214
  grid-template-columns: repeat(3, 1fr);
165
- gap: 0.75rem;
215
+ gap: .75rem;
216
+ position: relative;
217
+ z-index: 1;
166
218
  }
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);
219
+
220
+ /* Shine wrapper */
221
+ .shine {
222
+ padding: 1px;
223
+ border-radius: 14px;
224
+ background: radial-gradient(
225
+ 350px circle at var(--x, -9999px) var(--y, -9999px),
226
+ var(--ks-shine),
227
+ var(--ks-border) 80%
228
+ );
229
+ display: block;
176
230
  text-decoration: none;
177
- cursor: pointer;
178
- transition: border-color 0.15s;
179
231
  }
180
- .link-card:hover { border-color: #49c5a3; }
181
- .link-card__top {
182
- display: flex;
183
- justify-content: space-between;
184
- align-items: center;
232
+ .net-shine { grid-column: 1 / span 2; grid-row: 1 / span 3; }
233
+
234
+ /* Card base */
235
+ .card { border-radius: 13px; background: var(--ks-soft); height: 100%; }
236
+
237
+ /* Network card */
238
+ .net-card { padding: 1.75rem; display: flex; flex-direction: column; gap: 1.5rem; min-height: 320px; }
239
+ .net-top { display: flex; align-items: center; justify-content: space-between; }
240
+ .net-icon { font-size: 2.25rem; color: var(--ks-accent); line-height: 1; }
241
+ .badge { font-size: .7rem; font-weight: 500; padding: .2em .6em; border-radius: 999px; border: 1px solid; white-space: nowrap; }
242
+ .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
243
+ .stat { display: flex; flex-direction: column; gap: .25rem; }
244
+ .stat-label { font-size: .65rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); }
245
+ .stat-value { font-size: .95rem; color: var(--ks-heading); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
246
+ .mono { font-family: monospace; }
247
+
248
+ /* Link cards */
249
+ .link-card { padding: 1rem; display: flex; flex-direction: column; justify-content: space-between; gap: .5rem; min-height: 100px; }
250
+ .link-top { display: flex; justify-content: space-between; align-items: flex-start; }
251
+ .link-icon { color: var(--ks-accent); }
252
+ .link-arrow { color: var(--ks-muted); }
253
+ .link-label { font-size: .58rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ks-muted); margin-bottom: .1rem; }
254
+ .link-title { font-size: .875rem; font-weight: 600; color: var(--ks-heading); }
255
+ .link-desc { font-size: .75rem; color: var(--ks-muted); margin-top: .1rem; }
256
+
257
+ /* Dialog */
258
+ .dialog {
259
+ border: 1px solid var(--ks-border);
260
+ border-radius: 16px;
261
+ padding: 0;
262
+ width: min(440px, calc(100vw - 2rem));
263
+ max-width: none;
264
+ overflow: hidden;
265
+ background: var(--ks-surface);
266
+ color: var(--ks-text);
267
+ box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
268
+ }
269
+ .dialog-inner { padding: 2rem; position: relative; }
270
+ .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; }
271
+ .dialog-close:hover { color: var(--ks-heading); }
272
+ .dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; font-weight: 700; color: var(--ks-heading); }
273
+ .dialog-body { font-size: .875rem; color: var(--ks-muted); margin: 0 0 1.25rem; line-height: 1.65; }
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);
185
287
  }
186
- .link-card__label {
187
- font-size: .65rem;
188
- text-transform: uppercase;
189
- letter-spacing: .05em;
190
- color: var(--color-muted, #888);
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;
191
301
  }
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);
302
+ .copy-btn:hover { color: var(--ks-heading); border-color: var(--ks-heading); }
303
+ .copy-btn.copied { color: #4caf50; border-color: #4caf50; }
304
+
305
+ .dialog-thanks { font-size: .8rem; color: var(--ks-muted); margin: .75rem 0 0; text-align: center; }
306
+ </style>
307
+
308
+ <style>
309
+ .dialog::backdrop {
310
+ background: rgba(0, 0, 0, .5);
311
+ backdrop-filter: blur(4px);
197
312
  }
198
313
  </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
  }