A record or an artifact reference was serialised into the port row, and the panel widened until the type selects and the buttons beside them were pushed off its edge — a checkpoint reference is 130 characters of digest, and none of them are what you want while wiring a flow. What shows now is what the value *is*: 'artifact · weights.json · 60B', 'record · 3 fields'. A chevron unfolds the whole of it, wrapped, inside the panel it belongs to. A scalar still reads as itself, and scrolls its own overflow into view when it is longer than the room it was given. That behaviour already existed inside the edge inspector; it moves to Common/Marquee so the panel can have it too, and the inspector drops its own copy of the raw-JSON block along with it. The rows are smaller for it: the type select finally fits the word 'artifact', and a port nothing has come through on says so with a dash rather than a sentence — nine ports of 'nothing has come through yet' is a panel of prose about the absence of values. The e2e check asserts both halves: that the summary is what appears, and that the panel is still exactly 400px with the value unfolded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import { motion } from "motion/react"
|
|
import { useEffect, useRef, useState } from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
/** Pixels a second. Slow enough to read, quick enough not to be a wait. */
|
|
const SPEED = 25
|
|
/** Long enough to read the start before it moves, and again at the far end. */
|
|
const DWELL = 1.2
|
|
|
|
/**
|
|
* Text that scrolls its own overflow into view and back.
|
|
*
|
|
* A long message name, or a value that does not fit, would otherwise widen
|
|
* whatever holds it until the panel around it gives way. This keeps the width
|
|
* the layout asked for and moves the text instead — and stays perfectly still
|
|
* when it already fits, so a column of these is not a column of motion.
|
|
*
|
|
* Reduced motion is handled by the `MotionConfig reducedMotion="user"` each app
|
|
* root is wrapped in: the text simply sits at its start.
|
|
*/
|
|
export function Marquee({
|
|
text,
|
|
className,
|
|
}: {
|
|
text: string
|
|
className?: string
|
|
}) {
|
|
const ref = useRef<HTMLSpanElement>(null)
|
|
const [overflow, setOverflow] = useState(0)
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: a new string is what changes the measurement.
|
|
useEffect(() => {
|
|
const el = ref.current
|
|
if (!el) return
|
|
const measure = () =>
|
|
setOverflow(Math.max(0, el.scrollWidth - el.clientWidth))
|
|
measure()
|
|
// The panel it sits in can be resized, and the same text overflows or does
|
|
// not depending on how much room it was given.
|
|
const observer = new ResizeObserver(measure)
|
|
observer.observe(el)
|
|
return () => observer.disconnect()
|
|
}, [text])
|
|
|
|
return (
|
|
<span
|
|
ref={ref}
|
|
className={cn("min-w-0 overflow-hidden whitespace-nowrap", className)}
|
|
>
|
|
<motion.span
|
|
className="inline-block"
|
|
animate={{ x: -overflow }}
|
|
transition={
|
|
overflow
|
|
? {
|
|
duration: overflow / SPEED,
|
|
ease: "linear",
|
|
delay: DWELL,
|
|
repeat: Number.POSITIVE_INFINITY,
|
|
repeatType: "reverse",
|
|
repeatDelay: DWELL,
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
{text}
|
|
</motion.span>
|
|
</span>
|
|
)
|
|
}
|