@dbx-tools/ui-mastra 0.3.18 → 0.3.20

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
@@ -26,19 +26,19 @@
26
26
  "shiki": "^3.0.0",
27
27
  "sql-formatter": "^15.6.9",
28
28
  "streamdown": "^2.5.0",
29
- "@dbx-tools/shared-model": "0.3.18",
30
- "@dbx-tools/shared-core": "0.3.18",
31
- "@dbx-tools/ui-appkit": "0.3.18",
32
- "@dbx-tools/shared-mastra": "0.3.18",
33
- "@dbx-tools/shared-genie": "0.3.18",
34
- "@dbx-tools/ui-branding": "0.3.18"
29
+ "@dbx-tools/shared-core": "0.3.20",
30
+ "@dbx-tools/shared-genie": "0.3.20",
31
+ "@dbx-tools/shared-mastra": "0.3.20",
32
+ "@dbx-tools/shared-model": "0.3.20",
33
+ "@dbx-tools/ui-appkit": "0.3.20",
34
+ "@dbx-tools/ui-branding": "0.3.20"
35
35
  },
36
36
  "main": "index.ts",
37
37
  "license": "UNLICENSED",
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "0.3.18",
41
+ "version": "0.3.20",
42
42
  "types": "index.ts",
43
43
  "type": "module",
44
44
  "exports": {
@@ -140,9 +140,20 @@ export const ChatView = ({
140
140
  onFeedback,
141
141
  }: ChatViewProps) => {
142
142
  const [input, setInput] = useState("");
143
- // Id of the queued steer currently being dragged (native HTML5 drag), for
144
- // the reorder affordance + drop styling. Null when not dragging.
143
+ // Id of the queued steer currently being dragged (pointer drag), for the
144
+ // reorder affordance + drop styling. Null when not dragging. Pointer Events
145
+ // (not native HTML5 drag) so the grip works on touch as well as mouse -
146
+ // `draggable`/`onDrag*` never fire on a touchscreen.
145
147
  const [draggingSteerId, setDraggingSteerId] = useState<string | null>(null);
148
+ // Live DOM refs to each queued-steer chip, keyed by steer id, so a pointer
149
+ // drag can hit-test the pointer's Y against each chip's midpoint and reorder
150
+ // as the finger/cursor moves over a neighbour.
151
+ const steerChipRefs = useRef(new Map<string, HTMLDivElement>());
152
+ // Id of the steer under an active pointer drag, mirrored in a ref so the
153
+ // pointermove handler reads it synchronously. React state (`draggingSteerId`)
154
+ // only drives styling and lags a render behind the pointerdown, which on
155
+ // touch dropped the first moves and made the drag feel dead.
156
+ const draggingIdRef = useRef<string | null>(null);
146
157
  const scrollRef = useRef<HTMLDivElement>(null);
147
158
  const contentRef = useRef<HTMLDivElement>(null);
148
159
  // Composer textarea, auto-grown with its content up to the CSS `max-h`.
@@ -166,6 +177,10 @@ export const ChatView = ({
166
177
  );
167
178
  const loadMoreRef = useRef(onLoadMore);
168
179
  loadMoreRef.current = onLoadMore;
180
+ // Latest queued steers, read by the pointer-drag move handler so it reorders
181
+ // against the current queue rather than the order captured when the drag began.
182
+ const queuedSteersRef = useRef(queuedSteers);
183
+ queuedSteersRef.current = queuedSteers;
169
184
 
170
185
  // Jump the transcript to the bottom, marking the write as programmatic so
171
186
  // the resulting scroll event isn't mistaken for the user scrolling away.
@@ -176,6 +191,40 @@ export const ChatView = ({
176
191
  el.scrollTop = el.scrollHeight;
177
192
  }, []);
178
193
 
194
+ // Reorder the queued steers to place `draggingId` at the slot whose chip the
195
+ // pointer is currently over, hit-testing the pointer Y against each chip's
196
+ // vertical midpoint. Called continuously during a pointer drag so the queue
197
+ // reflows live under the finger/cursor, then committed via `onReorderSteers`.
198
+ const reorderSteersByPointer = useCallback(
199
+ (draggingId: string, pointerY: number) => {
200
+ if (!onReorderSteers) return;
201
+ const order = queuedSteersRef.current.map((s) => s.id);
202
+ // Build the target order: everything except the dragged id, then insert
203
+ // the dragged id before the first chip whose midpoint is below the
204
+ // pointer (or at the end if the pointer is past them all).
205
+ const rest = order.filter((id) => id !== draggingId);
206
+ let insertAt = rest.length;
207
+ for (let i = 0; i < rest.length; i += 1) {
208
+ const chip = steerChipRefs.current.get(rest[i]);
209
+ if (!chip) continue;
210
+ const box = chip.getBoundingClientRect();
211
+ if (pointerY < box.top + box.height / 2) {
212
+ insertAt = i;
213
+ break;
214
+ }
215
+ }
216
+ const next = [...rest];
217
+ next.splice(insertAt, 0, draggingId);
218
+ // Skip the commit when the order is unchanged, so we don't thrash the
219
+ // parent state on every pointermove tick.
220
+ if (next.length === order.length && next.every((id, i) => id === order[i])) {
221
+ return;
222
+ }
223
+ onReorderSteers(next);
224
+ },
225
+ [onReorderSteers],
226
+ );
227
+
179
228
  // Follow the bottom as streamed content grows, as long as we're "pinned".
180
229
  // A ResizeObserver on the transcript catches every height change - new
181
230
  // messages, token-by-token text, async markdown/chart layout, the waiting
@@ -655,59 +704,56 @@ export const ChatView = ({
655
704
  <div className="mb-2 flex flex-col gap-1">
656
705
  {queuedSteers.map((steer) => {
657
706
  const reorderable = Boolean(onReorderSteers);
658
- // Move the dragged steer to just before `steer` (or to the
659
- // end when dropped on itself at the tail) and report the new
660
- // id order to the driver.
661
- const dropBefore = () => {
662
- if (!onReorderSteers || !draggingSteerId || draggingSteerId === steer.id) {
663
- return;
664
- }
665
- const ids = queuedSteers.map((s) => s.id).filter((id) => id !== draggingSteerId);
666
- const at = ids.indexOf(steer.id);
667
- ids.splice(at === -1 ? ids.length : at, 0, draggingSteerId);
668
- onReorderSteers(ids);
669
- };
670
707
  return (
671
708
  <div
672
709
  key={steer.id}
673
- draggable={reorderable}
674
- onDragStart={
675
- reorderable
676
- ? (e) => {
677
- setDraggingSteerId(steer.id);
678
- e.dataTransfer.effectAllowed = "move";
679
- }
680
- : undefined
681
- }
682
- onDragOver={
683
- reorderable
684
- ? (e) => {
685
- e.preventDefault();
686
- e.dataTransfer.dropEffect = "move";
687
- }
688
- : undefined
689
- }
690
- onDrop={
691
- reorderable
692
- ? (e) => {
693
- e.preventDefault();
694
- dropBefore();
695
- setDraggingSteerId(null);
696
- }
697
- : undefined
698
- }
699
- onDragEnd={reorderable ? () => setDraggingSteerId(null) : undefined}
710
+ ref={(el) => {
711
+ if (el) steerChipRefs.current.set(steer.id, el);
712
+ else steerChipRefs.current.delete(steer.id);
713
+ }}
700
714
  className={cn(
701
715
  "flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs",
702
- reorderable && "cursor-grab active:cursor-grabbing",
703
716
  draggingSteerId === steer.id && "opacity-50",
704
717
  )}
705
718
  >
706
719
  {reorderable && (
707
- <GripVerticalIcon
708
- className="size-3 shrink-0 text-muted-foreground"
709
- aria-hidden="true"
710
- />
720
+ // Drag handle. Pointer Events (not native HTML5 drag) so
721
+ // it works on touch: `touch-none` (touch-action: none)
722
+ // stops the browser treating the drag as a scroll, and
723
+ // pointer capture keeps move/up events flowing to the grip
724
+ // even as the finger slides over sibling chips. The active
725
+ // id lives in a ref (`draggingIdRef`) so the first
726
+ // pointermove isn't dropped waiting for a state re-render.
727
+ // `-m-1 p-1` enlarges the tap target to ~28px without
728
+ // widening the visible grip - a 12px icon is too small to
729
+ // reliably grab on touch.
730
+ <span
731
+ role="button"
732
+ tabIndex={-1}
733
+ aria-label="Drag to reorder"
734
+ className="-m-1 shrink-0 cursor-grab touch-none p-1 text-muted-foreground active:cursor-grabbing"
735
+ onPointerDown={(e) => {
736
+ e.preventDefault();
737
+ e.currentTarget.setPointerCapture(e.pointerId);
738
+ draggingIdRef.current = steer.id;
739
+ setDraggingSteerId(steer.id);
740
+ }}
741
+ onPointerMove={(e) => {
742
+ if (draggingIdRef.current !== steer.id) return;
743
+ reorderSteersByPointer(steer.id, e.clientY);
744
+ }}
745
+ onPointerUp={(e) => {
746
+ e.currentTarget.releasePointerCapture(e.pointerId);
747
+ draggingIdRef.current = null;
748
+ setDraggingSteerId(null);
749
+ }}
750
+ onPointerCancel={() => {
751
+ draggingIdRef.current = null;
752
+ setDraggingSteerId(null);
753
+ }}
754
+ >
755
+ <GripVerticalIcon className="size-3" aria-hidden="true" />
756
+ </span>
711
757
  )}
712
758
  <span className="text-muted-foreground">Queued</span>
713
759
  <span className="min-w-0 flex-1 truncate">{steer.text}</span>