snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { Icon } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import type { JsonPathSegment } from "../core/Editor/json/jsonPath";
|
||||
import { IconButton } from "../core/IconButton";
|
||||
import { Tooltip } from "../core/Tooltip";
|
||||
|
||||
interface Props {
|
||||
segments: JsonPathSegment[];
|
||||
appliedFilter: string | null;
|
||||
/**
|
||||
* How many leading segments make up the applied filter. `null` when unfiltered, or
|
||||
* when the filter isn't a plain path and is shown as raw text instead.
|
||||
*/
|
||||
appliedDepth: number | null;
|
||||
filterError: boolean;
|
||||
/** Called with the number of leading segments to filter to. `0` clears the filter. */
|
||||
onSelect: (count: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows where the cursor sits inside a response (`$ > sort > [0] > id`). An applied
|
||||
* plain-path filter is the start of the trail, so any crumb moves it. While filtered,
|
||||
* the bar is tinted so a lingering filter is hard to miss.
|
||||
*/
|
||||
export function ResponseBreadcrumbBar({
|
||||
segments,
|
||||
appliedFilter,
|
||||
appliedDepth,
|
||||
filterError,
|
||||
onSelect,
|
||||
}: Props) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [overflow, setOverflow] = useState({ left: false, right: false });
|
||||
|
||||
const measure = useCallback(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el == null) return;
|
||||
const maxScroll = el.scrollWidth - el.clientWidth;
|
||||
setOverflow({
|
||||
left: el.scrollLeft > 1,
|
||||
right: el.scrollLeft < maxScroll - 1,
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keep the deepest crumb (where the cursor is) in view as the path changes.
|
||||
useLayoutEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el == null) return;
|
||||
el.scrollLeft = el.scrollWidth;
|
||||
measure();
|
||||
}, [segments, measure]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el == null) return;
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [measure]);
|
||||
|
||||
const scrollBy = useCallback((direction: -1 | 1) => {
|
||||
const el = scrollRef.current;
|
||||
if (el == null) return;
|
||||
el.scrollBy({ left: direction * Math.max(120, el.clientWidth * 0.6), behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
const isFiltered = appliedFilter != null;
|
||||
|
||||
return (
|
||||
<div className="rounded bg-surface shadow pointer-events-auto">
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-stretch h-6 rounded border overflow-hidden font-mono text-xs text-text-subtle select-none",
|
||||
!isFiltered && "border-border-subtle",
|
||||
isFiltered && !filterError && "border-primary/40 bg-primary/10",
|
||||
filterError && "border-danger/40 bg-danger/10",
|
||||
)}
|
||||
>
|
||||
{isFiltered && appliedDepth == null ? (
|
||||
<span
|
||||
className={classNames(
|
||||
"flex items-center min-w-0 truncate px-2",
|
||||
filterError ? "text-danger" : "text-primary",
|
||||
)}
|
||||
>
|
||||
{appliedFilter}
|
||||
</span>
|
||||
) : (
|
||||
<Crumb
|
||||
label="$"
|
||||
tooltip="Clear filter"
|
||||
isApplied={isFiltered}
|
||||
onClick={appliedDepth != null && appliedDepth > 0 ? () => onSelect(0) : null}
|
||||
/>
|
||||
)}
|
||||
{overflow.left && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon="chevron_left"
|
||||
title="Scroll breadcrumbs left"
|
||||
iconColor="secondary"
|
||||
onClick={() => scrollBy(-1)}
|
||||
className="shrink-0 h-auto!"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={measure}
|
||||
className="flex items-stretch min-w-0 overflow-x-auto hide-scrollbars whitespace-nowrap"
|
||||
>
|
||||
{segments.map((segment, i) => {
|
||||
const count = i + 1;
|
||||
const isApplied = appliedDepth != null && count <= appliedDepth;
|
||||
const isClickable = (!isFiltered || appliedDepth != null) && count !== appliedDepth;
|
||||
return (
|
||||
<div key={i} className="flex items-stretch shrink-0">
|
||||
<Icon
|
||||
icon="chevron_right"
|
||||
size="xs"
|
||||
className="text-text-subtlest shrink-0 self-center"
|
||||
/>
|
||||
<Crumb
|
||||
label={segment.kind === "index" ? `[${segment.index}]` : segment.key}
|
||||
tooltip={
|
||||
segment.kind === "index"
|
||||
? `Filter to element ${segment.index}`
|
||||
: `Filter to ${segment.key}`
|
||||
}
|
||||
isApplied={isApplied}
|
||||
onClick={isClickable ? () => onSelect(count) : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{overflow.right && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
icon="chevron_right"
|
||||
title="Scroll breadcrumbs right"
|
||||
iconColor="secondary"
|
||||
onClick={() => scrollBy(1)}
|
||||
className="shrink-0 h-auto!"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Crumb({
|
||||
label,
|
||||
tooltip,
|
||||
isApplied,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
tooltip: string;
|
||||
isApplied: boolean;
|
||||
onClick: (() => void) | null;
|
||||
}) {
|
||||
const outerClassName = classNames("flex items-center px-0.5 py-0.5", isApplied && "text-primary");
|
||||
const innerClassName = "flex items-center h-full px-1 rounded-sm";
|
||||
if (onClick == null) {
|
||||
return (
|
||||
<span className={classNames(outerClassName, "shrink-0")}>
|
||||
<span className={innerClassName}>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} className="shrink-0 items-stretch!">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={classNames(outerClassName, "group/crumb focus-visible:outline-none")}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
innerClassName,
|
||||
"transition-colors group-hover/crumb:text-text group-hover/crumb:bg-surface-highlight",
|
||||
"group-focus-visible/crumb:ring-1 group-focus-visible/crumb:ring-border-focus",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user