Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ export function QuestionPermission({
}
multiSelect={isOnSubmitStep ? false : isMultiSelect}
hideSubmitButton={isOnSubmitStep}
resizable
allowCustomInput={!isOnSubmitStep}
customInputPlaceholder="Type your answer..."
currentStep={activeStep}
Expand Down
95 changes: 93 additions & 2 deletions packages/ui/src/primitives/action-selector/ActionSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { compactHomePath } from "@posthog/shared";
import { Box, Flex, Text } from "@radix-ui/themes";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { isCancelOption, isSubmitOption } from "./constants";
import { OptionRow } from "./OptionRow";
import { StepTabs } from "./StepTabs";
import type { ActionSelectorProps } from "./types";
import { useActionSelectorState } from "./useActionSelectorState";

// Floor keeps the options and submit row visible even at the smallest size;
// ceiling matches the card's default `max-h-[80vh]` cap.
const MIN_CARD_HEIGHT = 160;
const MAX_CARD_HEIGHT_FRACTION = 0.8;

export function ActionSelector({
title,
pendingAction,
Expand All @@ -20,6 +25,7 @@ export function ActionSelector({
initialSelections,
initialCustomInput,
hideSubmitButton = false,
resizable = false,
onSelect,
onMultiSelect,
onCancel,
Expand Down Expand Up @@ -76,6 +82,68 @@ export function ActionSelector({
onCancel?.();
}, [onCancel]);

// User-chosen height in px once the card has been dragged; null means the
// card sizes naturally under its `max-h-[80vh]` cap.
const [cardHeight, setCardHeight] = useState<number | null>(null);
const [isResizing, setIsResizing] = useState(false);
const resizeStartRef = useRef({ y: 0, height: 0 });

const handleResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
const container = containerRef.current;
if (!container) return;
e.preventDefault();
resizeStartRef.current = {
y: e.clientY,
height: container.getBoundingClientRect().height,
};
setIsResizing(true);
document.body.style.cursor = "row-resize";
document.body.style.userSelect = "none";
},
[containerRef],
);

useEffect(() => {
if (!isResizing) return;

const handleMouseMove = (e: MouseEvent) => {
const { y, height } = resizeStartRef.current;
// Dragging up (clientY decreases) grows the card; down shrinks it,
// revealing more of the transcript above.
const next = height + (y - e.clientY);
const max = window.innerHeight * MAX_CARD_HEIGHT_FRACTION;
setCardHeight(Math.max(MIN_CARD_HEIGHT, Math.min(max, next)));
};

const handleMouseUp = () => {
setIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isResizing]);

// If the card unmounts mid-drag no mouseup fires — clear the global cursor
// and text-selection lock so the app isn't left stuck.
const isResizingRef = useRef(isResizing);
isResizingRef.current = isResizing;
useEffect(
() => () => {
if (isResizingRef.current) {
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
},
[],
);

const handlersRef = useRef({
moveUp,
moveDown,
Expand Down Expand Up @@ -226,9 +294,32 @@ export function ActionSelector({
}}
style={{
outline: "none",
...(resizable && cardHeight !== null ? { height: cardHeight } : {}),
}}
className="flex max-h-[80vh] flex-col rounded-(--radius-3) border border-(--gray-6) bg-(--gray-1)"
className="relative flex max-h-[80vh] flex-col rounded-(--radius-3) border border-(--gray-6) bg-(--gray-1)"
>
{resizable && (
// Drag handle riding the top edge — the card is anchored to the bottom
// of the chat, so dragging up grows it and dragging down shrinks it.
<Box
aria-hidden
onMouseDown={handleResizeMouseDown}
className="group absolute inset-x-0 top-0 z-10 flex h-2 cursor-row-resize items-start justify-center"
>
<span
className={`mt-0.5 h-1 w-10 rounded-full transition-colors ${
isResizing
? "bg-(--gray-8)"
: "bg-(--gray-6) group-hover:bg-(--gray-8)"
}`}
/>
</Box>
)}
{isResizing && (
// Keeps the row-resize cursor while the pointer crosses content that
// sets its own cursor.
<Box className="fixed inset-0 z-[200] cursor-row-resize" />
)}
<Flex direction="column" gap="2" className="min-h-0 flex-1">
{hasSteps && steps && (
<StepTabs
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/primitives/action-selector/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export interface ActionSelectorProps {
initialSelections?: string[];
initialCustomInput?: string;
hideSubmitButton?: boolean;
/**
* When true, the card can be resized vertically via a drag handle at its top
* edge: dragging down shrinks it (revealing more of the transcript above),
* dragging up grows it back (capped at 80vh). Inner content scrolls within
* the chosen height.
*/
resizable?: boolean;
onSelect: (optionId: string, customInput?: string) => void;
onMultiSelect?: (optionIds: string[], customInput?: string) => void;
onCancel?: () => void;
Expand Down
Loading