@starlove/orbit 1.3.4 → 1.3.5

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.
@@ -513,10 +513,13 @@ export function enableKanbanDragScroll() {
513
513
 
514
514
  kanban.addEventListener("pointerdown", (event) => {
515
515
  if (event.button !== 0) return;
516
+ // Clear stale drag-click suppression on any fresh primary press — even over
517
+ // a card — so a both-button / two-finger pan that emitted no trailing click
518
+ // can't swallow the next genuine click.
519
+ dragged = false;
516
520
  if (shouldIgnore(event.target)) return;
517
521
 
518
522
  active = true;
519
- dragged = false;
520
523
  pointerId = event.pointerId;
521
524
  startX = event.clientX;
522
525
  startScroll = kanban.scrollLeft;
@@ -569,4 +572,123 @@ export function enableKanbanDragScroll() {
569
572
  },
570
573
  true
571
574
  );
575
+
576
+ // --- Both-button mouse drag + two-finger touch drag -----------------------
577
+ // A second panning affordance that works ANYWHERE on the board, including
578
+ // directly over cards. A plain left-click / single-finger tap is left to the
579
+ // cards (so they still open + drag); holding *both* mouse buttons, or dragging
580
+ // with *two fingers*, pans the board horizontally without engaging a card.
581
+ let bothBtnPanning = false;
582
+ let bothBtnLastX = 0;
583
+ let suppressContextMenu = false;
584
+
585
+ // Pressing the second mouse button (buttons bitmask 1|2 === 3) arms the pan
586
+ // and cancels any single-button drag already in flight (see `active` above).
587
+ kanban.addEventListener("pointerdown", (event) => {
588
+ if (event.pointerType !== "mouse" || (event.buttons & 3) !== 3) return;
589
+ bothBtnPanning = true;
590
+ bothBtnLastX = event.clientX;
591
+ suppressContextMenu = true;
592
+ active = false; // hand control off from the left-button drag
593
+ kanban.classList.add("is-grabbing");
594
+ });
595
+
596
+ kanban.addEventListener("pointermove", (event) => {
597
+ if (event.pointerType !== "mouse") return;
598
+ const bothDown = (event.buttons & 3) === 3;
599
+ if (bothDown) {
600
+ event.preventDefault();
601
+ if (!bothBtnPanning) {
602
+ bothBtnPanning = true;
603
+ bothBtnLastX = event.clientX;
604
+ suppressContextMenu = true;
605
+ active = false;
606
+ kanban.classList.add("is-grabbing");
607
+ return;
608
+ }
609
+ const delta = event.clientX - bothBtnLastX;
610
+ if (delta) {
611
+ kanban.scrollLeft -= delta;
612
+ bothBtnLastX = event.clientX;
613
+ dragged = true; // eat the trailing synthetic click
614
+ }
615
+ } else if (bothBtnPanning) {
616
+ bothBtnPanning = false;
617
+ kanban.classList.remove("is-grabbing");
618
+ }
619
+ });
620
+
621
+ kanban.addEventListener("pointerup", (event) => {
622
+ if (event.pointerType === "mouse" && bothBtnPanning && (event.buttons & 3) !== 3) {
623
+ bothBtnPanning = false;
624
+ kanban.classList.remove("is-grabbing");
625
+ }
626
+ });
627
+
628
+ // Right-click handling on the board:
629
+ // - after a both-button pan, swallow the trailing context menu;
630
+ // - otherwise suppress the native browser menu everywhere EXCEPT over a card,
631
+ // which owns its own context menu (see bindCardContextMenuDelegated).
632
+ kanban.addEventListener("contextmenu", (event) => {
633
+ if (suppressContextMenu) {
634
+ event.preventDefault();
635
+ event.stopPropagation();
636
+ suppressContextMenu = false;
637
+ return;
638
+ }
639
+ const onCard = event.target instanceof Element && event.target.closest(".card");
640
+ if (!onCard) {
641
+ event.preventDefault();
642
+ event.stopPropagation();
643
+ }
644
+ });
645
+
646
+ // Two-finger touch pan. Touch events (not pointer events) are used here so we
647
+ // can reliably preventDefault the browser's native two-finger scroll/zoom.
648
+ let touchPanning = false;
649
+ let touchPanLastX = 0;
650
+ const twoFingerCentroidX = (touches) => (touches[0].clientX + touches[1].clientX) / 2;
651
+
652
+ kanban.addEventListener(
653
+ "touchstart",
654
+ (event) => {
655
+ // Fresh gesture — clear any leftover drag-click suppression so a real tap
656
+ // is never swallowed when the prior two-finger pan emitted no click.
657
+ dragged = false;
658
+ if (event.touches.length === 2) {
659
+ touchPanning = true;
660
+ touchPanLastX = twoFingerCentroidX(event.touches);
661
+ active = false; // cancel any single-finger drag that started
662
+ kanban.classList.add("is-grabbing");
663
+ } else {
664
+ touchPanning = false;
665
+ }
666
+ },
667
+ { passive: true }
668
+ );
669
+
670
+ kanban.addEventListener(
671
+ "touchmove",
672
+ (event) => {
673
+ if (!touchPanning || event.touches.length !== 2) return;
674
+ const x = twoFingerCentroidX(event.touches);
675
+ const delta = x - touchPanLastX;
676
+ if (delta) {
677
+ event.preventDefault();
678
+ kanban.scrollLeft -= delta;
679
+ touchPanLastX = x;
680
+ dragged = true; // eat the trailing tap so a card doesn't open
681
+ }
682
+ },
683
+ { passive: false }
684
+ );
685
+
686
+ const endTouchPan = (event) => {
687
+ if (event.touches.length < 2 && touchPanning) {
688
+ touchPanning = false;
689
+ kanban.classList.remove("is-grabbing");
690
+ }
691
+ };
692
+ kanban.addEventListener("touchend", endTouchPan, { passive: true });
693
+ kanban.addEventListener("touchcancel", endTouchPan, { passive: true });
572
694
  }
package/public/styles.css CHANGED
@@ -2376,10 +2376,20 @@ select.detail-meta-badge::-ms-expand {
2376
2376
  grid-area: tabs;
2377
2377
  display: flex;
2378
2378
  gap: 0.3rem;
2379
+ /* min-width:0 lets this grid item shrink to its column so the tabs overflow
2380
+ *internally* and scroll, rather than being clipped by the drawer's
2381
+ overflow:hidden. */
2382
+ min-width: 0;
2379
2383
  overflow-x: auto;
2384
+ overscroll-behavior-x: contain;
2385
+ -webkit-overflow-scrolling: touch;
2380
2386
  scrollbar-width: none;
2381
2387
  }
2382
2388
 
2389
+ .drawer-tabs::-webkit-scrollbar {
2390
+ display: none;
2391
+ }
2392
+
2383
2393
  .drawer-tabs[hidden] {
2384
2394
  display: none;
2385
2395
  }
@@ -4210,7 +4220,9 @@ details.section > summary .mcp-title-row {
4210
4220
 
4211
4221
  .lane-row {
4212
4222
  display: grid;
4213
- grid-template-columns: 2rem 2rem minmax(6rem, 1fr) minmax(5.5rem, 7rem) 2rem 2rem;
4223
+ /* ↑ ↓ | name (flex) | New | × — every control is a 2rem square except the
4224
+ name field, so the cluster stays tight at every width (fits phones too). */
4225
+ grid-template-columns: 2rem 2rem minmax(4rem, 1fr) 2rem 2rem;
4214
4226
  gap: 0.4rem;
4215
4227
  align-items: center;
4216
4228
  border: 1px solid var(--line);
@@ -4451,7 +4463,8 @@ button:disabled {
4451
4463
 
4452
4464
  .lane-create-form {
4453
4465
  display: grid;
4454
- grid-template-columns: minmax(8rem, 1fr) minmax(7rem, 9rem) auto;
4466
+ /* name field (flex) + Add Lane button — two children, two columns. */
4467
+ grid-template-columns: minmax(6rem, 1fr) auto;
4455
4468
  gap: 0.45rem;
4456
4469
  margin-top: 0.65rem;
4457
4470
  }
@@ -4873,22 +4886,47 @@ button.toast-close::after {
4873
4886
 
4874
4887
  /* Delay topbar wrap until small tablet/large phone widths. */
4875
4888
  @media (max-width: 800px) {
4889
+ /* Let the topbar wrap into rows instead of stacking the brand above the
4890
+ controls. Dissolving the `.controls` box (display:contents) promotes its
4891
+ children into the topbar's own wrap flow, so the brand and the notes strip
4892
+ share the first row (notes beside the badge, as on desktop) while search
4893
+ and the action buttons fall onto their own rows below. */
4876
4894
  .topbar {
4877
4895
  position: static;
4878
- align-items: stretch;
4879
- flex-direction: column;
4896
+ flex-wrap: wrap;
4897
+ align-items: center;
4898
+ justify-content: flex-start;
4899
+ column-gap: 0.5rem;
4900
+ row-gap: 0.55rem;
4880
4901
  }
4881
4902
 
4882
4903
  .controls {
4883
- flex-wrap: wrap;
4884
- justify-content: flex-start;
4904
+ display: contents;
4905
+ }
4906
+
4907
+ .topbar-controls-gap {
4908
+ display: none;
4885
4909
  }
4886
4910
 
4887
- .topbar-controls-gap,
4911
+ /* Clickable, truncated board-notes strip sits right after the badge, hugging
4912
+ its text and ellipsizing if it runs long. Hidden when empty so the badge
4913
+ keeps the row to itself (the logo still opens the notes popover). */
4888
4914
  .brand-notes-strip {
4915
+ flex: 0 1 auto;
4916
+ min-width: 0;
4917
+ --topbar-notes-max-width: 100%;
4918
+ }
4919
+
4920
+ .brand-notes-strip[data-empty="true"] {
4889
4921
  display: none;
4890
4922
  }
4891
4923
 
4924
+ /* Reclaim vertical space for the board on mobile. */
4925
+ .frame-footer {
4926
+ display: none;
4927
+ }
4928
+
4929
+ /* flex-basis 100% forces search onto its own full-width row below the brand. */
4892
4930
  .topbar-search {
4893
4931
  flex: 1 1 100%;
4894
4932
  width: 100%;
@@ -4896,14 +4934,12 @@ button.toast-close::after {
4896
4934
  max-width: none;
4897
4935
  }
4898
4936
 
4899
- /* On the wrapped action row, keep utility controls anchored to the right. */
4937
+ /* On the wrapped action-button row, anchor the utility controls to the right. */
4900
4938
  #themeToggle {
4901
4939
  margin-left: auto;
4902
4940
  }
4903
4941
 
4904
- .composer-row,
4905
- .lane-row,
4906
- .lane-create-form {
4942
+ .composer-row {
4907
4943
  grid-template-columns: 1fr 1fr;
4908
4944
  }
4909
4945
 
@@ -4917,10 +4953,16 @@ button.toast-close::after {
4917
4953
  flex-wrap: wrap;
4918
4954
  }
4919
4955
 
4956
+ /* Phone: collapse the action controls to icon-only so New Card, Board, theme,
4957
+ Help, and Settings all fit on a single line. */
4958
+ .btn-sun-label,
4959
+ .topbar-chip-label,
4960
+ .topbar-btn-label {
4961
+ display: none;
4962
+ }
4963
+
4920
4964
  .composer-row,
4921
- .deployment-actions,
4922
- .lane-row,
4923
- .lane-create-form {
4965
+ .deployment-actions {
4924
4966
  grid-template-columns: 1fr;
4925
4967
  }
4926
4968