@le-space/ui 0.7.0 → 0.8.0

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.
@@ -91,6 +91,44 @@ export let apiHosts = undefined
91
91
  )
92
92
  $: orphanRegistrations = state.orphanBootstrapRegistrations ?? []
93
93
 
94
+ // Which tab each expanded deployment card is showing, keyed by item hash.
95
+ let instanceTabs = {}
96
+
97
+ function instanceTab(itemHash) {
98
+ return instanceTabs[itemHash] ?? 'general'
99
+ }
100
+
101
+ function selectInstanceTab(itemHash, tab) {
102
+ instanceTabs = { ...instanceTabs, [itemHash]: tab }
103
+ if (tab !== 'general') {
104
+ // Addresses are live state — AutoTLS entries turn up a minute or two after
105
+ // the deploy reports success — so load on open and let the user refresh.
106
+ controller.loadInstanceAddresses(itemHash)
107
+ }
108
+ }
109
+
110
+ function addressesOf(itemHash) {
111
+ return state.instanceAddresses?.[itemHash] ?? null
112
+ }
113
+
114
+ /** Label the tab by what the deployment actually publishes, not by profile. */
115
+ function addressTabLabel(itemHash) {
116
+ const addresses = addressesOf(itemHash)
117
+ if (addresses?.groups?.length) return 'Multiaddresses'
118
+ if (addresses?.pwaEnv) return 'Endpoints'
119
+ return 'Multiaddresses'
120
+ }
121
+
122
+ function copyAllText(addresses) {
123
+ return (addresses?.groups ?? []).flatMap((group) => group.addresses).join('\n')
124
+ }
125
+
126
+ function pwaEnvText(addresses) {
127
+ return Object.entries(addresses?.pwaEnv ?? {})
128
+ .map(([key, value]) => `${key}=${value}`)
129
+ .join('\n')
130
+ }
131
+
94
132
  onMount(async () => {
95
133
  const unsubscribe = controller.subscribe((next) => {
96
134
  state = next
@@ -359,6 +397,38 @@ export let apiHosts = undefined
359
397
  </button>
360
398
  </div>
361
399
 
400
+ {@const itemHash = entry.instance.item_hash}
401
+ {@const addresses = addressesOf(itemHash)}
402
+ {@const tab = instanceTab(itemHash)}
403
+
404
+ <div class="tab-bar" role="tablist">
405
+ <button
406
+ type="button"
407
+ role="tab"
408
+ class="tab"
409
+ class:tab-active={tab === 'general'}
410
+ aria-selected={tab === 'general'}
411
+ on:click={() => selectInstanceTab(itemHash, 'general')}
412
+ >General</button>
413
+ <button
414
+ type="button"
415
+ role="tab"
416
+ class="tab"
417
+ class:tab-active={tab === 'addresses'}
418
+ aria-selected={tab === 'addresses'}
419
+ on:click={() => selectInstanceTab(itemHash, 'addresses')}
420
+ >{addressTabLabel(itemHash)}</button>
421
+ <button
422
+ type="button"
423
+ role="tab"
424
+ class="tab"
425
+ class:tab-active={tab === 'health'}
426
+ aria-selected={tab === 'health'}
427
+ on:click={() => selectInstanceTab(itemHash, 'health')}
428
+ >Health</button>
429
+ </div>
430
+
431
+ {#if tab === 'general'}
362
432
  <div class="instance-grid">
363
433
  <div>
364
434
  <span>Host IPv4</span>
@@ -389,12 +459,109 @@ export let apiHosts = undefined
389
459
  <strong>{joinMappedPorts(entry.details.mappedPorts)}</strong>
390
460
  </div>
391
461
 
462
+ {#if entry.details.proxyHostname}
463
+ <div class="mono-block">
464
+ <span>Proxy hostname</span>
465
+ <strong>{entry.details.proxyHostname}</strong>
466
+ <CopyButton text={entry.details.proxyHostname} />
467
+ </div>
468
+ {/if}
469
+
392
470
  {#if bootstrapUiEnabled() && confirmedRegistration}
393
471
  <div class="mono-block">
394
472
  <span>Bootstrap Registration</span>
395
473
  <strong>{shortHash(confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? 'confirmed', 14, 8)}</strong>
396
474
  </div>
397
475
  {/if}
476
+ {/if}
477
+
478
+ {#if tab === 'addresses'}
479
+ <div class="tab-head">
480
+ <span>{addresses?.sourceUrl ?? 'Reading from the deployment'}</span>
481
+ <button
482
+ type="button"
483
+ class="refresh"
484
+ disabled={addresses?.status === 'loading'}
485
+ on:click={() => controller.loadInstanceAddresses(itemHash, { force: true })}
486
+ >{addresses?.status === 'loading' ? 'Loading…' : 'Refresh'}</button>
487
+ </div>
488
+
489
+ {#if addresses?.status === 'loading' && !addresses?.groups?.length}
490
+ <p class="tab-note">Reading the deployment's address document…</p>
491
+ {:else if addresses?.status === 'unsupported'}
492
+ <p class="tab-note">{addresses.error}</p>
493
+ {:else if addresses?.status === 'error'}
494
+ <p class="alert error">{addresses.error}</p>
495
+ {:else if addresses?.groups?.length}
496
+ {#if addresses.peerId}
497
+ <div class="mono-block">
498
+ <span>Peer ID</span>
499
+ <strong>{addresses.peerId}</strong>
500
+ <CopyButton text={addresses.peerId} />
501
+ </div>
502
+ {/if}
503
+
504
+ {#each addresses.groups as group}
505
+ <div class="address-group">
506
+ <div class="address-group-head">
507
+ <strong>{group.label}</strong>
508
+ <CopyButton text={group.addresses.join('\n')} label="Copy group" />
509
+ </div>
510
+ {#each group.addresses as address}
511
+ <div class="address-row">
512
+ <code>{address}</code>
513
+ <CopyButton text={address} />
514
+ </div>
515
+ {/each}
516
+ </div>
517
+ {/each}
518
+
519
+ <div class="link-row">
520
+ <CopyButton text={copyAllText(addresses)} label="Copy all" />
521
+ <CopyButton text={JSON.stringify(addresses.payload, null, 2)} label="Copy JSON" />
522
+ </div>
523
+ {:else if addresses?.pwaEnv}
524
+ <div class="address-group">
525
+ <div class="address-group-head">
526
+ <strong>PWA environment</strong>
527
+ <CopyButton text={pwaEnvText(addresses)} label="Copy block" />
528
+ </div>
529
+ {#each Object.entries(addresses.pwaEnv) as [key, value]}
530
+ <div class="address-row">
531
+ <code>{key}={value}</code>
532
+ <CopyButton text={`${key}=${value}`} />
533
+ </div>
534
+ {/each}
535
+ </div>
536
+ {:else}
537
+ <p class="tab-note">This deployment reports no addresses yet.</p>
538
+ {/if}
539
+ {/if}
540
+
541
+ {#if tab === 'health'}
542
+ <div class="tab-head">
543
+ <span>{addresses?.sourceUrl ?? 'Reading from the deployment'}</span>
544
+ <button
545
+ type="button"
546
+ class="refresh"
547
+ disabled={addresses?.status === 'loading'}
548
+ on:click={() => controller.loadInstanceAddresses(itemHash, { force: true })}
549
+ >{addresses?.status === 'loading' ? 'Loading…' : 'Refresh'}</button>
550
+ </div>
551
+
552
+ {#if addresses?.payload}
553
+ <pre class="payload">{JSON.stringify(addresses.payload, null, 2)}</pre>
554
+ <div class="link-row">
555
+ <CopyButton text={JSON.stringify(addresses.payload, null, 2)} label="Copy JSON" />
556
+ </div>
557
+ {:else if addresses?.status === 'loading'}
558
+ <p class="tab-note">Reading the deployment's health document…</p>
559
+ {:else if addresses?.error}
560
+ <p class="tab-note">{addresses.error}</p>
561
+ {:else}
562
+ <p class="tab-note">No health document read yet.</p>
563
+ {/if}
564
+ {/if}
398
565
 
399
566
  <div class="link-row">
400
567
  <CopyButton text={entry.instance.item_hash} label="Copy hash" />
@@ -800,6 +967,14 @@ export let apiHosts = undefined
800
967
  font: 500 0.8125rem/1.4 var(--relay-font-body);
801
968
  }
802
969
 
970
+ /* The list, not the closed control. Without this it inherits the select's
971
+ translucent field background and keeps --relay-text, which on a light
972
+ drop-down is light-on-light. */
973
+ option {
974
+ background: var(--relay-menu-bg);
975
+ color: var(--relay-text);
976
+ }
977
+
803
978
  input:focus-visible,
804
979
  select:focus-visible,
805
980
  textarea:focus-visible {
@@ -830,6 +1005,115 @@ export let apiHosts = undefined
830
1005
  gap: 0.3rem;
831
1006
  }
832
1007
 
1008
+ .tab-bar {
1009
+ display: flex;
1010
+ gap: 0.35rem;
1011
+ margin: 0.75rem 0 0.5rem;
1012
+ border-bottom: 1px solid var(--relay-surface-border);
1013
+ }
1014
+
1015
+ .tab {
1016
+ border: 0;
1017
+ border-bottom: 2px solid transparent;
1018
+ background: transparent;
1019
+ color: var(--relay-muted);
1020
+ padding: 0.35rem 0.6rem;
1021
+ font: 700 0.6875rem/1 var(--relay-font-mono);
1022
+ text-transform: uppercase;
1023
+ letter-spacing: 0.06em;
1024
+ cursor: pointer;
1025
+ }
1026
+
1027
+ .tab:hover {
1028
+ color: var(--relay-text);
1029
+ }
1030
+
1031
+ .tab-active {
1032
+ color: var(--relay-text);
1033
+ border-bottom-color: var(--relay-cyan);
1034
+ }
1035
+
1036
+ .tab-head {
1037
+ display: flex;
1038
+ align-items: center;
1039
+ justify-content: space-between;
1040
+ gap: 0.5rem;
1041
+ margin-bottom: 0.5rem;
1042
+ }
1043
+
1044
+ .tab-head span {
1045
+ color: var(--relay-muted);
1046
+ font: 400 0.6875rem/1.4 var(--relay-font-mono);
1047
+ overflow-wrap: anywhere;
1048
+ }
1049
+
1050
+ .refresh {
1051
+ border: 1px solid var(--relay-surface-border);
1052
+ border-radius: 999px;
1053
+ background: transparent;
1054
+ color: var(--relay-muted);
1055
+ padding: 0.28rem 0.7rem;
1056
+ font: 700 0.6563rem/1 var(--relay-font-mono);
1057
+ text-transform: uppercase;
1058
+ letter-spacing: 0.06em;
1059
+ cursor: pointer;
1060
+ }
1061
+
1062
+ .refresh:hover:not(:disabled) {
1063
+ color: var(--relay-text);
1064
+ border-color: var(--relay-cyan);
1065
+ }
1066
+
1067
+ .refresh:disabled {
1068
+ opacity: 0.6;
1069
+ cursor: default;
1070
+ }
1071
+
1072
+ .tab-note {
1073
+ color: var(--relay-muted);
1074
+ font-size: 0.75rem;
1075
+ margin: 0.5rem 0;
1076
+ }
1077
+
1078
+ .address-group {
1079
+ margin-bottom: 0.75rem;
1080
+ }
1081
+
1082
+ .address-group-head {
1083
+ display: flex;
1084
+ align-items: center;
1085
+ justify-content: space-between;
1086
+ gap: 0.5rem;
1087
+ margin-bottom: 0.3rem;
1088
+ }
1089
+
1090
+ .address-group-head strong {
1091
+ font-size: 0.75rem;
1092
+ }
1093
+
1094
+ .address-row {
1095
+ display: flex;
1096
+ align-items: center;
1097
+ justify-content: space-between;
1098
+ gap: 0.5rem;
1099
+ padding: 0.25rem 0;
1100
+ border-top: 1px solid var(--relay-surface-border);
1101
+ }
1102
+
1103
+ .address-row code {
1104
+ font: 400 0.6875rem/1.4 var(--relay-font-mono);
1105
+ overflow-wrap: anywhere;
1106
+ }
1107
+
1108
+ .payload {
1109
+ max-height: 20rem;
1110
+ overflow: auto;
1111
+ padding: 0.5rem;
1112
+ border: 1px solid var(--relay-surface-border);
1113
+ border-radius: 0.5rem;
1114
+ font: 400 0.6875rem/1.5 var(--relay-font-mono);
1115
+ }
1116
+
833
1117
  .deployment-box strong {
834
1118
  font-family: var(--relay-font-mono);
835
1119
  font-size: 0.75rem;
@@ -42,6 +42,11 @@
42
42
  --relay-surface: rgba(11, 14, 21, 0.55);
43
43
  --relay-surface-border: var(--relay-horizon);
44
44
  --relay-field-bg: rgba(11, 14, 21, 0.85);
45
+ /* Opaque on purpose. A select's drop-down takes the control's own colours on
46
+ the platforms that honour author styling, and it opens over the page rather
47
+ than over the panel - a translucent background would composite against
48
+ whatever is behind it and leave light text on a light list. */
49
+ --relay-menu-bg: var(--relay-deep-space);
45
50
  --relay-accent: var(--relay-coral);
46
51
  --relay-accent-contrast: #0b0e15;
47
52
  --relay-link: var(--relay-cyan);
@@ -81,6 +86,7 @@
81
86
  --relay-surface: rgba(20, 27, 46, 0.04);
82
87
  --relay-surface-border: #d9e0ec;
83
88
  --relay-field-bg: #ffffff;
89
+ --relay-menu-bg: #ffffff;
84
90
  --relay-accent: var(--relay-coral);
85
91
  --relay-accent-contrast: #ffffff;
86
92
  --relay-link: var(--relay-cyan);