@livedesk/client 0.1.50 → 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.
@@ -428,10 +428,12 @@ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
428
428
  </html>`;
429
429
  }
430
430
 
431
- function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
431
+ function renderConnectionChoicePage({ error = '', pin = '', slot = '' } = {}) {
432
432
  const errorBlock = error
433
433
  ? `<div class="error">${escapeHtml(error)}</div>`
434
434
  : '';
435
+ const normalizedSlot = normalizeSlotNumber(slot);
436
+ const slotLabel = normalizedSlot ? `Slot ${String(normalizedSlot).padStart(3, '0')}` : 'First available';
435
437
  return `<!doctype html>
436
438
  <html lang="en">
437
439
  <head>
@@ -443,48 +445,99 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
443
445
  * { box-sizing: border-box; }
444
446
  html, body { width: 100%; min-height: 100%; margin: 0; }
445
447
  body {
446
- display: grid;
447
- place-items: center;
448
- padding: 24px;
449
- background:
450
- radial-gradient(circle at 50% 0%, rgba(148, 163, 184, 0.22), transparent 34%),
451
- linear-gradient(180deg, #f8fafc, #eef2f7);
448
+ min-height: 100vh;
449
+ background: #f6f7f9;
452
450
  color: #111827;
453
451
  }
452
+ .shell {
453
+ min-height: 100vh;
454
+ display: grid;
455
+ grid-template-columns: minmax(360px, 462px) minmax(0, 1fr);
456
+ }
454
457
  main {
455
- width: min(440px, 100%);
458
+ display: flex;
459
+ flex-direction: column;
460
+ justify-content: center;
461
+ min-height: 100vh;
462
+ padding: 42px 54px;
463
+ border-right: 1px solid #d8dde5;
464
+ background: rgba(255, 255, 255, 0.96);
465
+ box-shadow: 18px 0 55px rgba(15, 23, 42, 0.08);
466
+ }
467
+ .stack {
468
+ width: min(100%, 386px);
456
469
  display: grid;
457
- gap: 18px;
458
- border: 1px solid rgba(148, 163, 184, 0.26);
459
- border-radius: 18px;
460
- padding: 28px;
461
- background: rgba(255, 255, 255, 0.92);
462
- box-shadow: 0 24px 70px rgba(15, 23, 42, 0.14);
470
+ gap: 24px;
471
+ }
472
+ .topbar {
473
+ position: absolute;
474
+ top: 28px;
475
+ left: 54px;
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 11px;
463
479
  }
464
480
  .brand {
465
481
  display: flex;
466
482
  align-items: center;
467
- gap: 12px;
483
+ gap: 13px;
468
484
  }
469
485
  .mark {
470
- width: 42px;
471
- height: 42px;
486
+ width: 38px;
487
+ height: 38px;
472
488
  display: grid;
473
489
  place-items: center;
474
- border-radius: 10px;
475
- background: #111827;
490
+ border-radius: 9px;
491
+ background: linear-gradient(135deg, #2dd4bf, #2563eb);
476
492
  color: #ffffff;
477
493
  font-weight: 950;
494
+ box-shadow: 0 12px 28px rgba(37, 99, 235, 0.18);
478
495
  }
479
- h1 { margin: 0; font-size: 28px; line-height: 1.05; letter-spacing: 0; }
480
- p { margin: 5px 0 0; color: #64748b; font-size: 14px; line-height: 1.45; font-weight: 650; }
496
+ .brand-copy {
497
+ display: grid;
498
+ gap: 2px;
499
+ line-height: 1;
500
+ }
501
+ .brand-copy strong {
502
+ font-size: 15px;
503
+ letter-spacing: 0;
504
+ }
505
+ .brand-copy span {
506
+ color: #64748b;
507
+ font-size: 12px;
508
+ font-weight: 750;
509
+ }
510
+ .headline {
511
+ display: grid;
512
+ gap: 9px;
513
+ padding-top: 16px;
514
+ }
515
+ h1 { margin: 0; font-size: 32px; line-height: 1.08; letter-spacing: 0; }
516
+ p { margin: 0; color: #64748b; font-size: 14px; line-height: 1.55; font-weight: 650; }
481
517
  .panel {
482
518
  display: grid;
483
- gap: 12px;
484
- border: 1px solid rgba(148, 163, 184, 0.22);
485
- border-radius: 14px;
486
- padding: 14px;
519
+ gap: 14px;
520
+ }
521
+ .placement {
522
+ display: flex;
523
+ align-items: center;
524
+ justify-content: space-between;
525
+ min-height: 38px;
526
+ padding: 0 12px;
527
+ border: 1px solid #d9dee7;
528
+ border-radius: 8px;
487
529
  background: #ffffff;
530
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
531
+ }
532
+ .placement span {
533
+ color: #64748b;
534
+ font-size: 12px;
535
+ font-weight: 800;
536
+ }
537
+ .placement strong {
538
+ color: #111827;
539
+ font-size: 13px;
540
+ font-weight: 900;
488
541
  }
489
542
  form {
490
543
  display: grid;
@@ -501,12 +554,13 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
501
554
  letter-spacing: 0.04em;
502
555
  }
503
556
  input {
504
- height: 48px;
505
- border: 1px solid rgba(148, 163, 184, 0.45);
506
- border-radius: 12px;
507
- padding: 0 14px;
557
+ height: 42px;
558
+ border: 1px solid #cfd6e1;
559
+ border-radius: 8px;
560
+ padding: 0 12px;
561
+ background: rgba(17, 24, 39, 0.026);
508
562
  color: #111827;
509
- font-size: 24px;
563
+ font-size: 22px;
510
564
  font-weight: 950;
511
565
  letter-spacing: 0.18em;
512
566
  text-align: center;
@@ -517,25 +571,36 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
517
571
  box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
518
572
  }
519
573
  button, .google {
520
- height: 44px;
574
+ height: 42px;
521
575
  display: inline-flex;
522
576
  align-items: center;
523
577
  justify-content: center;
524
578
  gap: 8px;
525
- border: 0;
526
- border-radius: 12px;
579
+ border-radius: 8px;
527
580
  background: #111827;
528
581
  color: #ffffff;
529
582
  font-size: 14px;
530
583
  font-weight: 900;
531
584
  text-decoration: none;
532
585
  cursor: pointer;
586
+ transition: border-color 160ms ease, background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
587
+ }
588
+ button {
589
+ border: 1px solid #111827;
590
+ box-shadow: 0 10px 24px rgba(17, 24, 39, 0.12);
591
+ }
592
+ button:hover, .google:hover {
593
+ transform: translateY(-1px);
533
594
  }
534
595
  .google {
535
596
  background: #ffffff;
536
- border: 1px solid rgba(148, 163, 184, 0.35);
597
+ border: 1px solid #cfd6e1;
537
598
  color: #111827;
538
599
  }
600
+ .google:hover {
601
+ border-color: #111827;
602
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
603
+ }
539
604
  .divider {
540
605
  display: grid;
541
606
  grid-template-columns: 1fr auto 1fr;
@@ -550,11 +615,11 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
550
615
  .divider::before, .divider::after {
551
616
  content: "";
552
617
  height: 1px;
553
- background: rgba(148, 163, 184, 0.3);
618
+ background: #d9dee7;
554
619
  }
555
620
  .error {
556
- border: 1px solid rgba(220, 38, 38, 0.22);
557
- border-radius: 12px;
621
+ border: 1px solid rgba(220, 38, 38, 0.24);
622
+ border-radius: 8px;
558
623
  padding: 10px 12px;
559
624
  background: rgba(254, 226, 226, 0.75);
560
625
  color: #991b1b;
@@ -567,31 +632,130 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
567
632
  font-size: 12px;
568
633
  font-weight: 750;
569
634
  }
635
+ aside {
636
+ display: grid;
637
+ place-items: center;
638
+ min-height: 100vh;
639
+ padding: 56px;
640
+ background:
641
+ radial-gradient(circle at 58% 28%, rgba(203, 213, 225, 0.5), transparent 28%),
642
+ linear-gradient(180deg, #f8fafc, #eef2f7);
643
+ }
644
+ .preview {
645
+ width: min(560px, 90%);
646
+ display: grid;
647
+ gap: 14px;
648
+ padding: 18px;
649
+ border: 1px solid rgba(203, 213, 225, 0.76);
650
+ border-radius: 14px;
651
+ background: rgba(255, 255, 255, 0.76);
652
+ box-shadow: 0 28px 90px rgba(15, 23, 42, 0.13);
653
+ }
654
+ .preview-title {
655
+ display: flex;
656
+ align-items: center;
657
+ justify-content: space-between;
658
+ color: #475569;
659
+ font-size: 12px;
660
+ font-weight: 900;
661
+ text-transform: uppercase;
662
+ letter-spacing: 0.06em;
663
+ }
664
+ .preview-dot {
665
+ width: 9px;
666
+ height: 9px;
667
+ border-radius: 999px;
668
+ background: #14b8a6;
669
+ box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
670
+ }
671
+ .screen-grid {
672
+ display: grid;
673
+ grid-template-columns: repeat(2, minmax(0, 1fr));
674
+ gap: 12px;
675
+ }
676
+ .screen {
677
+ aspect-ratio: 16 / 9;
678
+ border: 1px solid #d8dee8;
679
+ border-radius: 8px;
680
+ background:
681
+ linear-gradient(rgba(148, 163, 184, 0.11) 1px, transparent 1px),
682
+ linear-gradient(90deg, rgba(148, 163, 184, 0.11) 1px, transparent 1px),
683
+ #ffffff;
684
+ background-size: 32px 32px;
685
+ box-shadow: inset 0 -16px 26px rgba(15, 23, 42, 0.04);
686
+ }
687
+ .caption {
688
+ color: #64748b;
689
+ font-size: 13px;
690
+ font-weight: 700;
691
+ line-height: 1.55;
692
+ }
693
+ @media (max-width: 860px) {
694
+ .shell { grid-template-columns: 1fr; }
695
+ main {
696
+ min-height: 100vh;
697
+ padding: 84px 28px 36px;
698
+ border-right: 0;
699
+ }
700
+ .topbar {
701
+ left: 28px;
702
+ top: 24px;
703
+ }
704
+ .stack { justify-self: center; }
705
+ aside { display: none; }
706
+ }
570
707
  </style>
571
708
  </head>
572
709
  <body>
573
- <main>
574
- <div class="brand">
575
- <div class="mark">LD</div>
576
- <div>
577
- <h1>Connect LiveDesk</h1>
578
- <p>Use Google for your own Hub, or enter the 6-digit PIN shown on the screen wall.</p>
710
+ <div class="shell">
711
+ <main>
712
+ <div class="topbar">
713
+ <div class="mark">LD</div>
714
+ <div class="brand-copy">
715
+ <strong>LiveDesk</strong>
716
+ <span>Client connect</span>
717
+ </div>
579
718
  </div>
580
- </div>
581
- ${errorBlock}
582
- <section class="panel">
583
- <form method="post" action="/pin">
584
- <label>
585
- Pairing PIN
586
- <input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
587
- </label>
588
- <button type="submit">Connect with PIN</button>
589
- </form>
590
- </section>
591
- <div class="divider">or</div>
592
- <a class="google" href="/google">Continue with Google</a>
593
- <small>LiveDesk Client</small>
594
- </main>
719
+ <div class="stack">
720
+ <div class="headline">
721
+ <h1>Connect this computer</h1>
722
+ <p>Use the same Google account as your Hub, or enter the 6-digit Hub PIN from the screen wall.</p>
723
+ </div>
724
+ ${errorBlock}
725
+ <section class="panel">
726
+ <a class="google" href="/google">Continue with Google</a>
727
+ <div class="divider">or</div>
728
+ <form method="post" action="/pin">
729
+ <label>
730
+ Hub PIN
731
+ <input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
732
+ </label>
733
+ <button type="submit">Connect with PIN</button>
734
+ </form>
735
+ <div class="placement">
736
+ <span>Placement</span>
737
+ <strong>${escapeHtml(slotLabel)}</strong>
738
+ </div>
739
+ </section>
740
+ <small>This tab can stay open until the connection is ready.</small>
741
+ </div>
742
+ </main>
743
+ <aside aria-hidden="true">
744
+ <div class="preview">
745
+ <div class="preview-title">
746
+ <span>Screen wall ready</span>
747
+ <span class="preview-dot"></span>
748
+ </div>
749
+ <div class="screen-grid">
750
+ <div class="screen"></div>
751
+ <div class="screen"></div>
752
+ <div class="screen"></div>
753
+ <div class="screen"></div>
754
+ </div>
755
+ <div class="caption">LiveDesk places this computer on the Hub wall after sign-in or PIN approval.</div>
756
+ </div>
757
+ </aside>
758
+ </div>
595
759
  </body>
596
760
  </html>`;
597
761
  }
@@ -655,6 +819,7 @@ async function resolveManagerFromPin(supabase, pin) {
655
819
  async function startConnectionChoiceServer(supabase, options = {}) {
656
820
  const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
657
821
  const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
822
+ const slot = normalizeSlotNumber(options.slot);
658
823
  let listeningPort = port;
659
824
  let completed = false;
660
825
  let completedTitle = 'LiveDesk connection complete';
@@ -673,10 +838,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
673
838
  settleChoice(choice);
674
839
  setImmediate(() => server.close());
675
840
  };
841
+ const renderChoice = (props = {}) => renderConnectionChoicePage({ slot, ...props });
676
842
 
677
843
  const handleError = (res, error, pin = '') => {
678
844
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
679
- res.end(renderConnectionChoicePage({
845
+ res.end(renderChoice({
680
846
  error: error instanceof Error ? error.message : String(error),
681
847
  pin
682
848
  }));
@@ -701,7 +867,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
701
867
 
702
868
  if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
703
869
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
704
- res.end(renderConnectionChoicePage());
870
+ res.end(renderChoice());
705
871
  return;
706
872
  }
707
873
 
@@ -776,7 +942,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
776
942
  }
777
943
  if (!code) {
778
944
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
779
- res.end(renderConnectionChoicePage({
945
+ res.end(renderChoice({
780
946
  error: 'Google did not return an auth code. Try Google sign-in again.'
781
947
  }));
782
948
  return;
@@ -805,7 +971,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
805
971
  }
806
972
 
807
973
  res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
808
- res.end(renderConnectionChoicePage({
974
+ res.end(renderChoice({
809
975
  error: 'Unknown LiveDesk connection route.'
810
976
  }));
811
977
  })().catch(err => {
@@ -846,7 +1012,10 @@ async function startConnectionChoiceServer(supabase, options = {}) {
846
1012
  }
847
1013
 
848
1014
  async function chooseClientConnection(supabase, options = {}) {
849
- const connectionPage = await startConnectionChoiceServer(supabase, { authPort: options.authPort });
1015
+ const connectionPage = await startConnectionChoiceServer(supabase, {
1016
+ authPort: options.authPort,
1017
+ slot: options.slot
1018
+ });
850
1019
  console.log('Opening LiveDesk connection page...');
851
1020
  openBrowser(connectionPage.url);
852
1021
  return connectionPage.waitForChoice;
@@ -976,7 +1145,10 @@ async function prepareLoginConnection(parsed) {
976
1145
 
977
1146
  if (shouldLogin) {
978
1147
  const supabase = await createSupabaseClient();
979
- const choice = await chooseClientConnection(supabase, { authPort: parsed.authPort });
1148
+ const choice = await chooseClientConnection(supabase, {
1149
+ authPort: parsed.authPort,
1150
+ slot: parsed.slot
1151
+ });
980
1152
  if (choice.type === 'pin') {
981
1153
  manager = choice.manager;
982
1154
  pair = choice.pair;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {