@le-space/ui 0.1.52

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/styles.css ADDED
@@ -0,0 +1,17 @@
1
+ :root {
2
+ --relay-font-heading:
3
+ "Epilogue", "Avenir Next", "Segoe UI", sans-serif;
4
+ --relay-font-body:
5
+ "DM Sans", "Inter", "Segoe UI", sans-serif;
6
+ --relay-font-mono:
7
+ "DM Mono", "SFMono-Regular", "Menlo", monospace;
8
+ --relay-panel-bg: rgba(12, 20, 31, 0.92);
9
+ --relay-panel-border: rgba(255, 255, 255, 0.12);
10
+ --relay-panel-shadow: 0 28px 80px rgba(3, 8, 20, 0.45);
11
+ --relay-text: #f8fafc;
12
+ --relay-muted: #9fb2ca;
13
+ --relay-blue: #0176ce;
14
+ --relay-red: #e91315;
15
+ --relay-yellow: #ffc83f;
16
+ --relay-green: #37d67a;
17
+ }
@@ -0,0 +1,492 @@
1
+ <script>
2
+ import { onDestroy, onMount } from 'svelte'
3
+
4
+ import { createSponsorRelayController, formatDateTime, formatNumber, joinMappedPorts, shortHash } from '../shared/index'
5
+ import AccordionSection from './components/AccordionSection.svelte'
6
+ import CopyButton from './components/CopyButton.svelte'
7
+ import LauncherButton from './components/LauncherButton.svelte'
8
+ import StatusLed from './components/StatusLed.svelte'
9
+ import './styles/theme.css'
10
+
11
+ export let libp2p = null
12
+ export let manifestUrl = './rootfs-manifest.json'
13
+ export let manifestJson = ''
14
+ export let sshPublicKey = ''
15
+ export let instanceName = 'sponsor-relay'
16
+ export let showInstances = true
17
+ export let openByDefault = false
18
+ export let launcherMode = 'floating'
19
+ export let apiHost = undefined
20
+ export let crnListUrl = undefined
21
+ export let schedulerApiHost = undefined
22
+ export let twoN6ApiHost = undefined
23
+
24
+ const controller = createSponsorRelayController({
25
+ libp2p,
26
+ manifestUrl,
27
+ manifestJson,
28
+ sshPublicKey,
29
+ instanceName,
30
+ showInstances,
31
+ openByDefault,
32
+ launcherMode,
33
+ apiHost,
34
+ crnListUrl,
35
+ schedulerApiHost,
36
+ twoN6ApiHost
37
+ })
38
+
39
+ let state = controller.getState()
40
+
41
+ onMount(async () => {
42
+ const unsubscribe = controller.subscribe((next) => {
43
+ state = next
44
+ })
45
+
46
+ await controller.init()
47
+ return unsubscribe
48
+ })
49
+
50
+ onDestroy(() => {
51
+ controller.destroy()
52
+ })
53
+ </script>
54
+
55
+ <LauncherButton open={state.open} onToggle={() => controller.toggleOpen()} mode={launcherMode} />
56
+
57
+ {#if state.open}
58
+ <div class="backdrop" on:click={() => controller.setOpen(false)}></div>
59
+ {/if}
60
+
61
+ {#if state.open}
62
+ <aside class="panel">
63
+ <div class="panel-head">
64
+ <div>
65
+ <p class="eyebrow">Aleph VM credit deployer</p>
66
+ <h2>Sponsor Relay</h2>
67
+ </div>
68
+ <button class="refresh" type="button" on:click={() => controller.refresh()} disabled={state.busy.refreshing}>
69
+ {state.busy.refreshing ? 'Syncing' : 'Refresh'}
70
+ </button>
71
+ </div>
72
+
73
+ <div class="status-strip">
74
+ <div class="status-pill">
75
+ <StatusLed tone={state.wallet.connected ? 'ok' : 'error'} />
76
+ <div>
77
+ <strong>{state.wallet.connected ? shortHash(state.wallet.address, 6, 4) : 'MetaMask disconnected'}</strong>
78
+ <small>{state.wallet.connected ? 'Credit-only wallet active' : 'Connect wallet to continue'}</small>
79
+ </div>
80
+ </div>
81
+ <div class="status-pill">
82
+ <StatusLed tone={state.rootfsHealth.tone} />
83
+ <div>
84
+ <strong>{state.rootfsHealth.label}</strong>
85
+ <small>{state.rootfsHealth.detail ?? 'No rootfs details yet'}</small>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="ping-strip">
91
+ <div>
92
+ <span class="mini-label">relay ping sent</span>
93
+ <div class="mini-row"><StatusLed tone={state.relayPing.sent ? 'caution' : state.relayPing.tone} pulse={state.relayPing.sent} /><strong>{state.relayPing.sent ? 'sent' : 'idle'}</strong></div>
94
+ </div>
95
+ <div>
96
+ <span class="mini-label">relay pong received</span>
97
+ <div class="mini-row"><StatusLed tone={state.relayPing.received ? 'ok' : state.relayPing.tone} pulse={state.relayPing.received} /><strong>{state.relayPing.received ? `${formatNumber(state.relayPing.lastLatencyMs, 0)} ms` : 'waiting'}</strong></div>
98
+ </div>
99
+ </div>
100
+
101
+ {#if state.errorText}
102
+ <p class="alert error">{state.errorText}</p>
103
+ {/if}
104
+ <p class="status-text">{state.statusText}</p>
105
+
106
+ <div class="grid">
107
+ <label class="field">
108
+ <span>Manifest URL</span>
109
+ <input value={state.manifestUrl} on:input={(event) => controller.setManifestUrl(event.currentTarget.value)} />
110
+ </label>
111
+ <label class="field">
112
+ <span>Instance Name</span>
113
+ <input value={state.instanceName} on:input={(event) => controller.setInstanceName(event.currentTarget.value)} />
114
+ </label>
115
+ <label class="field">
116
+ <span>Tier</span>
117
+ <select value={state.pricingSummary.tier?.id ?? state.tierId} on:change={(event) => controller.setTierId(event.currentTarget.value)}>
118
+ {#each state.pricingSummary.pricing?.tiers ?? [] as tier}
119
+ <option value={tier.id}>{tier.id}</option>
120
+ {/each}
121
+ </select>
122
+ </label>
123
+ <label class="field wide">
124
+ <span>SSH Public Key</span>
125
+ <textarea rows="3" on:input={(event) => controller.setSshPublicKey(event.currentTarget.value)}>{state.sshPublicKey}</textarea>
126
+ </label>
127
+ </div>
128
+
129
+ <AccordionSection title="Paste Manifest" open={state.showPasteManifest}>
130
+ <label class="field wide">
131
+ <span>Pasted rootfs manifest JSON</span>
132
+ <textarea rows="7" on:input={(event) => controller.setManifestJson(event.currentTarget.value)}>{state.manifestJson}</textarea>
133
+ </label>
134
+ </AccordionSection>
135
+
136
+ <div class="metrics">
137
+ <div class="metric-card">
138
+ <span>Credits</span>
139
+ <strong>{formatNumber(state.pricingSummary.availableCredits, 0)} available</strong>
140
+ <small>{formatNumber(state.pricingSummary.requiredCredits, 0)} required</small>
141
+ </div>
142
+ <div class="metric-card">
143
+ <span>Tier spec</span>
144
+ <strong>{formatNumber(state.pricingSummary.vcpus, 0)} vCPU · {formatNumber(state.pricingSummary.memoryMiB, 0)} MiB</strong>
145
+ <small>{formatNumber(state.pricingSummary.diskMiB, 0)} MiB disk</small>
146
+ </div>
147
+ <div class="metric-card">
148
+ <span>CRN</span>
149
+ <strong>{state.selectedCrn?.name ?? shortHash(state.selectedCrn?.hash)}</strong>
150
+ <small>{state.selectedCrn?.address ?? 'Auto-picked best compatible CRN'}</small>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="actions">
155
+ {#if state.wallet.connected}
156
+ <button class="primary" type="button" on:click={() => controller.deploy()} disabled={state.busy.deploying || !state.rootfsVerified}>
157
+ {state.busy.deploying ? 'Deploying…' : 'Deploy Relay'}
158
+ </button>
159
+ {:else}
160
+ <button class="primary" type="button" on:click={() => controller.connectWallet()} disabled={state.busy.connectingWallet}>
161
+ {state.busy.connectingWallet ? 'Connecting…' : 'Connect MetaMask'}
162
+ </button>
163
+ {/if}
164
+ </div>
165
+
166
+ {#if state.lastDeploymentHash}
167
+ <div class="deployment-box">
168
+ <span>Latest deployment</span>
169
+ <strong>{shortHash(state.lastDeploymentHash)}</strong>
170
+ <CopyButton text={state.lastDeploymentHash} label="Copy hash" />
171
+ </div>
172
+ {/if}
173
+
174
+ {#if state.showInstances}
175
+ <section class="instances">
176
+ <div class="section-head">
177
+ <div>
178
+ <h3>Instances</h3>
179
+ <small>{state.instances.length} deployment{state.instances.length === 1 ? '' : 's'}</small>
180
+ </div>
181
+ </div>
182
+
183
+ {#if state.instances.length === 0}
184
+ <p class="empty">Connect a wallet to load current deployments.</p>
185
+ {/if}
186
+
187
+ {#each state.instances as entry}
188
+ <AccordionSection title={`${entry.instance.content?.metadata?.name ?? 'relay'} · ${shortHash(entry.instance.item_hash)}`} open={true}>
189
+ <div class="instance-topline">
190
+ <div class="chip-row">
191
+ <span class="chip">{entry.details.messageStatus}</span>
192
+ {#if entry.details.crnUrl}
193
+ <span class="chip">{entry.details.crnUrl.replace(/^https?:\/\//, '')}</span>
194
+ {/if}
195
+ </div>
196
+ <button
197
+ class="delete"
198
+ type="button"
199
+ disabled={state.busy.deletingInstanceHash === entry.instance.item_hash}
200
+ on:click={() => controller.deleteInstance(entry.instance.item_hash)}
201
+ >
202
+ {state.busy.deletingInstanceHash === entry.instance.item_hash ? 'Deleting…' : 'Delete'}
203
+ </button>
204
+ </div>
205
+
206
+ <div class="instance-grid">
207
+ <div>
208
+ <span>Host IPv4</span>
209
+ <strong>{entry.details.hostIpv4 ?? '-'}</strong>
210
+ </div>
211
+ <div>
212
+ <span>IPv6</span>
213
+ <strong>{entry.details.ipv6 ?? '-'}</strong>
214
+ </div>
215
+ <div>
216
+ <span>VM IPv4</span>
217
+ <strong>{entry.details.vmIpv4 ?? '-'}</strong>
218
+ </div>
219
+ <div>
220
+ <span>Submitted</span>
221
+ <strong>{formatDateTime(entry.instance.reception_time ?? entry.instance.time)}</strong>
222
+ </div>
223
+ </div>
224
+
225
+ <div class="mono-block">
226
+ <span>SSH</span>
227
+ <strong>{entry.details.sshCommand ?? '-'}</strong>
228
+ <CopyButton text={entry.details.sshCommand ?? ''} />
229
+ </div>
230
+
231
+ <div class="mono-block">
232
+ <span>Mapped ports</span>
233
+ <strong>{joinMappedPorts(entry.details.mappedPorts)}</strong>
234
+ </div>
235
+
236
+ <div class="link-row">
237
+ <CopyButton text={entry.instance.item_hash} label="Copy hash" />
238
+ {#if entry.details.webUrl}
239
+ <a href={entry.details.webUrl} target="_blank" rel="noreferrer">Web</a>
240
+ {/if}
241
+ <a href={`https://api2.aleph.im/api/v0/messages/${entry.instance.item_hash}`} target="_blank" rel="noreferrer">API</a>
242
+ <a href={`https://explorer.aleph.cloud/address/ETH/${entry.instance.sender}/message/INSTANCE/${entry.instance.item_hash}`} target="_blank" rel="noreferrer">Explorer</a>
243
+ </div>
244
+
245
+ {#if entry.details.error}
246
+ <p class="alert error">{entry.details.error}</p>
247
+ {/if}
248
+ </AccordionSection>
249
+ {/each}
250
+ </section>
251
+ {/if}
252
+ </aside>
253
+ {/if}
254
+
255
+ <style>
256
+ .backdrop {
257
+ position: fixed;
258
+ inset: 0;
259
+ z-index: 9998;
260
+ background: radial-gradient(circle at 88% 82%, rgba(233, 19, 21, 0.18), transparent 34%);
261
+ backdrop-filter: blur(2px);
262
+ }
263
+
264
+ .panel {
265
+ position: fixed;
266
+ right: 1.4rem;
267
+ bottom: 11.5rem;
268
+ z-index: 9999;
269
+ width: min(28rem, calc(100vw - 2rem));
270
+ max-height: calc(100vh - 12.5rem);
271
+ overflow: auto;
272
+ border: 1px solid var(--relay-panel-border);
273
+ border-radius: 1.6rem;
274
+ background: var(--relay-panel-bg);
275
+ box-shadow: var(--relay-panel-shadow);
276
+ color: var(--relay-text);
277
+ padding: 1rem;
278
+ font-family: var(--relay-font-body);
279
+ }
280
+
281
+ .panel-head,
282
+ .status-strip,
283
+ .ping-strip,
284
+ .actions,
285
+ .section-head,
286
+ .instance-topline,
287
+ .link-row {
288
+ display: flex;
289
+ gap: 0.8rem;
290
+ align-items: center;
291
+ justify-content: space-between;
292
+ }
293
+
294
+ .eyebrow,
295
+ .mini-label,
296
+ .field span,
297
+ .metric-card span,
298
+ .mono-block span,
299
+ .instance-grid span,
300
+ .section-head small {
301
+ color: var(--relay-muted);
302
+ font-size: 0.72rem;
303
+ text-transform: uppercase;
304
+ letter-spacing: 0.08em;
305
+ }
306
+
307
+ h2,
308
+ h3,
309
+ strong {
310
+ margin: 0;
311
+ font-family: var(--relay-font-heading);
312
+ }
313
+
314
+ h2 {
315
+ font-size: 1.5rem;
316
+ }
317
+
318
+ .refresh,
319
+ .primary,
320
+ .delete {
321
+ border: 1px solid rgba(255, 255, 255, 0.12);
322
+ border-radius: 0.95rem;
323
+ padding: 0.7rem 0.9rem;
324
+ cursor: pointer;
325
+ color: var(--relay-text);
326
+ background: rgba(255, 255, 255, 0.05);
327
+ }
328
+
329
+ .primary {
330
+ width: 100%;
331
+ background: linear-gradient(135deg, var(--relay-blue), var(--relay-red));
332
+ font-weight: 700;
333
+ }
334
+
335
+ .delete {
336
+ background: rgba(233, 19, 21, 0.15);
337
+ color: #ffd4d4;
338
+ }
339
+
340
+ .status-strip,
341
+ .metrics {
342
+ display: grid;
343
+ grid-template-columns: 1fr 1fr;
344
+ gap: 0.75rem;
345
+ margin-top: 0.95rem;
346
+ }
347
+
348
+ .status-pill,
349
+ .metric-card {
350
+ display: grid;
351
+ gap: 0.3rem;
352
+ border: 1px solid rgba(255, 255, 255, 0.08);
353
+ border-radius: 1rem;
354
+ padding: 0.8rem;
355
+ background: rgba(255, 255, 255, 0.035);
356
+ }
357
+
358
+ .status-pill {
359
+ grid-template-columns: auto 1fr;
360
+ align-items: center;
361
+ gap: 0.7rem;
362
+ }
363
+
364
+ .ping-strip {
365
+ margin-top: 0.9rem;
366
+ padding: 0.85rem;
367
+ border-radius: 1rem;
368
+ background: rgba(1, 118, 206, 0.08);
369
+ }
370
+
371
+ .mini-row {
372
+ display: flex;
373
+ gap: 0.5rem;
374
+ align-items: center;
375
+ }
376
+
377
+ .alert {
378
+ margin: 0.8rem 0 0;
379
+ padding: 0.75rem 0.85rem;
380
+ border-radius: 0.9rem;
381
+ background: rgba(233, 19, 21, 0.12);
382
+ color: #ffd9d9;
383
+ }
384
+
385
+ .status-text {
386
+ color: var(--relay-muted);
387
+ margin: 0.65rem 0 0;
388
+ }
389
+
390
+ .grid {
391
+ display: grid;
392
+ grid-template-columns: 1fr 1fr;
393
+ gap: 0.75rem;
394
+ margin-top: 1rem;
395
+ }
396
+
397
+ .field {
398
+ display: grid;
399
+ gap: 0.4rem;
400
+ }
401
+
402
+ .field.wide {
403
+ grid-column: 1 / -1;
404
+ }
405
+
406
+ input,
407
+ select,
408
+ textarea {
409
+ width: 100%;
410
+ border: 1px solid rgba(255, 255, 255, 0.12);
411
+ border-radius: 0.95rem;
412
+ background: rgba(255, 255, 255, 0.05);
413
+ color: var(--relay-text);
414
+ padding: 0.75rem 0.85rem;
415
+ font: 500 0.9rem/1.35 var(--relay-font-body);
416
+ }
417
+
418
+ textarea,
419
+ .mono-block strong {
420
+ font-family: var(--relay-font-mono);
421
+ }
422
+
423
+ .actions,
424
+ .deployment-box,
425
+ .instances {
426
+ margin-top: 1rem;
427
+ }
428
+
429
+ .deployment-box,
430
+ .mono-block,
431
+ .instance-grid {
432
+ display: grid;
433
+ gap: 0.3rem;
434
+ }
435
+
436
+ .instance-grid {
437
+ grid-template-columns: 1fr 1fr;
438
+ margin: 0.75rem 0;
439
+ }
440
+
441
+ .chip-row {
442
+ display: flex;
443
+ gap: 0.4rem;
444
+ flex-wrap: wrap;
445
+ }
446
+
447
+ .chip {
448
+ border-radius: 999px;
449
+ padding: 0.25rem 0.55rem;
450
+ background: rgba(255, 255, 255, 0.08);
451
+ color: var(--relay-text);
452
+ font-size: 0.72rem;
453
+ }
454
+
455
+ .link-row {
456
+ justify-content: flex-start;
457
+ flex-wrap: wrap;
458
+ margin-top: 0.7rem;
459
+ }
460
+
461
+ .link-row a {
462
+ color: #bde0ff;
463
+ text-decoration: none;
464
+ font-weight: 700;
465
+ }
466
+
467
+ .empty {
468
+ color: var(--relay-muted);
469
+ }
470
+
471
+ button:disabled {
472
+ opacity: 0.6;
473
+ cursor: not-allowed;
474
+ }
475
+
476
+ @media (max-width: 640px) {
477
+ .panel {
478
+ right: 0.8rem;
479
+ left: 0.8rem;
480
+ width: auto;
481
+ bottom: 7.4rem;
482
+ max-height: calc(100vh - 8.4rem);
483
+ }
484
+
485
+ .grid,
486
+ .metrics,
487
+ .status-strip,
488
+ .instance-grid {
489
+ grid-template-columns: 1fr;
490
+ }
491
+ }
492
+ </style>
@@ -0,0 +1,37 @@
1
+ <script>
2
+ export let title = ''
3
+ export let open = false
4
+ </script>
5
+
6
+ <details class="accordion" {open}>
7
+ <summary>{title}</summary>
8
+ <div class="accordion-body">
9
+ <slot />
10
+ </div>
11
+ </details>
12
+
13
+ <style>
14
+ .accordion {
15
+ border: 1px solid var(--relay-panel-border);
16
+ border-radius: 1rem;
17
+ background: rgba(255, 255, 255, 0.035);
18
+ }
19
+
20
+ summary {
21
+ cursor: pointer;
22
+ list-style: none;
23
+ padding: 0.8rem 0.95rem;
24
+ color: var(--relay-text);
25
+ font: 700 0.8rem/1.1 var(--relay-font-heading);
26
+ text-transform: uppercase;
27
+ letter-spacing: 0.08em;
28
+ }
29
+
30
+ summary::-webkit-details-marker {
31
+ display: none;
32
+ }
33
+
34
+ .accordion-body {
35
+ padding: 0 0.95rem 0.95rem;
36
+ }
37
+ </style>
@@ -0,0 +1,31 @@
1
+ <script>
2
+ export let text = ''
3
+ export let label = 'Copy'
4
+
5
+ let copied = false
6
+
7
+ async function handleCopy() {
8
+ if (!text) return
9
+ await navigator.clipboard.writeText(text)
10
+ copied = true
11
+ setTimeout(() => {
12
+ copied = false
13
+ }, 1200)
14
+ }
15
+ </script>
16
+
17
+ <button type="button" class="copy-button" on:click={handleCopy}>
18
+ {copied ? 'Copied' : label}
19
+ </button>
20
+
21
+ <style>
22
+ .copy-button {
23
+ border: 1px solid rgba(255, 255, 255, 0.18);
24
+ border-radius: 999px;
25
+ background: rgba(255, 255, 255, 0.04);
26
+ color: var(--relay-text);
27
+ padding: 0.28rem 0.7rem;
28
+ font: 600 0.73rem/1 var(--relay-font-body);
29
+ cursor: pointer;
30
+ }
31
+ </style>
@@ -0,0 +1,44 @@
1
+ <script>
2
+ export let open = false
3
+ export let onToggle = () => {}
4
+ export let mode = 'floating'
5
+ </script>
6
+
7
+ <button class={`launcher ${mode} ${open ? 'open' : ''}`} type="button" on:click={onToggle}>
8
+ <span>Sponsor Relay</span>
9
+ </button>
10
+
11
+ <style>
12
+ .launcher {
13
+ border: 2px solid rgba(255, 255, 255, 0.88);
14
+ border-radius: 999px;
15
+ padding: 0.9rem 1.2rem;
16
+ background: linear-gradient(135deg, var(--relay-red) 0%, var(--relay-yellow) 100%);
17
+ color: white;
18
+ box-shadow: 0 18px 40px rgba(233, 19, 21, 0.28);
19
+ font: 700 0.84rem/1 var(--relay-font-heading);
20
+ letter-spacing: 0.08em;
21
+ text-transform: uppercase;
22
+ cursor: pointer;
23
+ transition: transform 180ms ease;
24
+ }
25
+
26
+ .launcher.floating {
27
+ position: fixed;
28
+ right: 1.4rem;
29
+ bottom: 5.8rem;
30
+ z-index: 10000;
31
+ }
32
+
33
+ .launcher.inline {
34
+ position: relative;
35
+ z-index: auto;
36
+ padding: 0.55rem 0.9rem;
37
+ font-size: 0.78rem;
38
+ }
39
+
40
+ .launcher:hover,
41
+ .launcher.open {
42
+ transform: translateY(-2px) scale(1.02);
43
+ }
44
+ </style>
@@ -0,0 +1,50 @@
1
+ <script>
2
+ export let tone = 'idle'
3
+ export let pulse = false
4
+ </script>
5
+
6
+ <span class={`status-led ${tone} ${pulse ? 'pulse' : ''}`}></span>
7
+
8
+ <style>
9
+ .status-led {
10
+ width: 0.65rem;
11
+ height: 0.65rem;
12
+ border-radius: 999px;
13
+ display: inline-block;
14
+ box-shadow: 0 0 0 0.18rem rgba(255, 255, 255, 0.06);
15
+ background: #64748b;
16
+ }
17
+
18
+ .status-led.ok {
19
+ background: var(--relay-green);
20
+ }
21
+
22
+ .status-led.caution {
23
+ background: var(--relay-yellow);
24
+ }
25
+
26
+ .status-led.error {
27
+ background: var(--relay-red);
28
+ }
29
+
30
+ .status-led.idle {
31
+ background: var(--relay-blue);
32
+ }
33
+
34
+ .pulse {
35
+ animation: relay-pulse 1.6s ease-in-out infinite;
36
+ }
37
+
38
+ @keyframes relay-pulse {
39
+ 0%,
40
+ 100% {
41
+ transform: scale(1);
42
+ opacity: 0.75;
43
+ }
44
+
45
+ 50% {
46
+ transform: scale(1.25);
47
+ opacity: 1;
48
+ }
49
+ }
50
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default } from './SponsorRelayFab.svelte'
2
+ export { default as SponsorRelayFab } from './SponsorRelayFab.svelte'
@@ -0,0 +1,17 @@
1
+ :root {
2
+ --relay-font-heading:
3
+ "Epilogue", "Avenir Next", "Segoe UI", sans-serif;
4
+ --relay-font-body:
5
+ "DM Sans", "Inter", "Segoe UI", sans-serif;
6
+ --relay-font-mono:
7
+ "DM Mono", "SFMono-Regular", "Menlo", monospace;
8
+ --relay-panel-bg: rgba(12, 20, 31, 0.92);
9
+ --relay-panel-border: rgba(255, 255, 255, 0.12);
10
+ --relay-panel-shadow: 0 28px 80px rgba(3, 8, 20, 0.45);
11
+ --relay-text: #f8fafc;
12
+ --relay-muted: #9fb2ca;
13
+ --relay-blue: #0176ce;
14
+ --relay-red: #e91315;
15
+ --relay-yellow: #ffc83f;
16
+ --relay-green: #37d67a;
17
+ }