Skip to content
Merged
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
70 changes: 65 additions & 5 deletions resources/js/components/graph-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
GraphNode,
LeavingRow,
NodeAnimation,
Overhang,
Point,
} from '@/lib/graph';
import { cn } from '@/lib/utils';
Expand Down Expand Up @@ -337,6 +338,38 @@ function snap(value: number): number {
return Math.round(value * ratio) / ratio;
}

/**
* What a node marks `data-graph-overhang` as hanging off it, like a LIVE badge over the bottom of an avatar's ring, grown
* by the ring the attribute gives, in px.
*/
function overhangsOf(element: HTMLElement, box: DOMRect): Overhang[] {
return [
...element.querySelectorAll<HTMLElement>('[data-graph-overhang]'),
].map((overhang) => {
const rect = overhang.getBoundingClientRect();
const ring = Number(overhang.dataset.graphOverhang) || 0;

return {
x: snap(rect.left - box.left - ring),
y: snap(rect.top - box.top - ring),
width: snap(rect.width + 2 * ring),
height: snap(rect.height + 2 * ring),
radius:
(Number.parseFloat(
getComputedStyle(overhang).borderTopLeftRadius,
) || 0) + ring,
};
});
}

function isOverhang(node: Node): boolean {
return (
node instanceof HTMLElement &&
(node.dataset.graphOverhang !== undefined ||
node.querySelector('[data-graph-overhang]') !== null)
);
}

/**
* The CSS animations running on a node and on the elements around it, up to the canvas, with the time they have left.
*/
Expand Down Expand Up @@ -420,11 +453,13 @@ export function GraphCanvas({ children }: { children: ReactNode }) {
.filter(({ element }) => element.isConnected)
.map(({ element, spec }) => {
const centre = layoutCentre(element, container);
const overhangs = overhangsOf(element, box);
rows.find((row) => row.element.contains(element))?.row.ids.add(
spec.id,
);

return {
...(overhangs.length > 0 && { overhangs }),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
id: spec.id,
lane: spec.lane,
after: spec.after,
Expand Down Expand Up @@ -545,7 +580,8 @@ export function GraphCanvas({ children }: { children: ReactNode }) {

// Nodes also move without anything resizing, like cards trading places when the feed reorders. Other changes
// inside a card, like an image done loading, move no node unless they resize the container, which the resize
// observer reports.
// observer reports. An overhang, like the LIVE badge as its user goes live, comes and goes out of the layout,
// so nothing resizes either.
const mutations = new MutationObserver((records) => {
const elements = [
...[...entries.current.values()].map(({ element }) => element),
Expand All @@ -554,11 +590,15 @@ export function GraphCanvas({ children }: { children: ReactNode }) {
departed.current.clear();

if (
records.some((record) =>
[...record.addedNodes, ...record.removedNodes].some(
(node) =>
records.some(
({ target, addedNodes, removedNodes }) =>
[...addedNodes, ...removedNodes].some((node) =>
elements.some((element) => node.contains(element)),
),
) ||
([...addedNodes, ...removedNodes].some(isOverhang) &&
elements.some((element) =>
element.contains(target),
)),
)
) {
measureNow();
Expand Down Expand Up @@ -696,6 +736,26 @@ export function GraphCanvas({ children }: { children: ReactNode }) {
)}
/>
))}
{nodes.flatMap((node) =>
(node.overhangs ?? []).map(
(overhang, index) => (
<rect
key={`${node.id}-${index}`}
x={overhang.x}
y={overhang.y}
width={overhang.width}
height={overhang.height}
rx={overhang.radius}
fill="black"
clipPath={clipOf(
leaving.findIndex((row) =>
row.ids.has(node.id),
),
)}
/>
),
),
)}
</mask>
{leaving.map((row, index) => (
<clipPath key={index} id={`${id}-leaving-${index}`}>
Expand Down
4 changes: 3 additions & 1 deletion resources/js/components/live-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { ComponentProps } from 'react';
import { cn } from '@/lib/utils';

export function LiveBadge({ className }: { className?: string }) {
export function LiveBadge({ className, ...props }: ComponentProps<'span'>) {
return (
<span
className={cn(
'inline-flex shrink-0 items-center gap-1 rounded-sm bg-red-600 px-1.5 py-px font-mono text-[10px] leading-4 font-semibold tracking-wide text-white uppercase',
className,
)}
data-test="live-badge"
{...props}
>
<span
aria-hidden
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/live-stream-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export function LiveAvatarMarker({ className }: { className?: string }) {
'absolute -bottom-1.5 left-1/2 -translate-x-1/2 ring-2 ring-card',
className,
)}
// On a graph node, it hangs below the ring: the lines go round it and its 2px ring.
data-graph-overhang="2"
/>
);
}
Expand Down
8 changes: 8 additions & 0 deletions resources/js/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export type GraphNode = Point & {
dimmed?: boolean;
/** On its way out, like an end dot scaling out as the thread goes on below it: linked to nothing, it only keeps its cut-out. */
isLeaving?: boolean;
/** What hangs off the node beyond its ring, like a LIVE badge, cut out of the lines too. */
overhangs?: Overhang[];
};

export type Overhang = Point & {
width: number;
height: number;
radius: number;
};

export type GraphEdge = {
Expand Down