Skip to content
Open
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
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.3/schema.json",

Check notice on line 2 in biome.json

View workflow job for this annotation

GitHub Actions / quality

deserialize

The configuration schema version does not match the CLI version 2.3.8
"files": {
"includes": [
"**/*.js",
Expand All @@ -12,6 +12,7 @@
"frontend/**/*.ts",
"!frontend/packages",
"!rivetkit-openapi/openapi.json",
"rivetkit-typescript/**/devtools/**/*.tsx",
"!scripts",
"!website",
"!site"
Expand Down
1,030 changes: 549 additions & 481 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions rivetkit-typescript/packages/devtools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# RivetKit DevTools


## Contributing

To contribute to the RivetKit DevTools package, please follow these steps:

1. Set up assets server for the `dist` folder:
```bash
pnpm dlx serve dist
```

2. Set your `CUSTOM_RIVETKIT_DEVTOOLS_URL` environment variable to point to the assets server (default is `http://localhost:3000`):
```bash
export CUSTOM_RIVETKIT_DEVTOOLS_URL=http://localhost:5000
```

This will ensure that the RivetKit will use local devtool assets instead of fetching them from the CDN.

3. In another terminal, run the development build:
```bash
pnpm dev
```

or run the production build:
```bash
pnpm build
```
48 changes: 48 additions & 0 deletions rivetkit-typescript/packages/devtools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@rivetkit/devtools",
"private": true,
"version": "2.0.24-rc.1",
"description": "RivetKit DevTools - A set of development tools for RivetKit",
"license": "Apache-2.0",
"keywords": [
"rivetkit"
],
"sideEffects": [
"./dist/chunk-*.js",
"./dist/chunk-*.cjs"
],
"files": [
"dist",
"package.json"
],
"exports": {
".": {
"import": {
"types": "./dist/mod.d.mts",
"default": "./dist/mod.mjs"
},
"require": {
"types": "./dist/mod.d.ts",
"default": "./dist/mod.js"
}
}
},
"scripts": {
"build": "tsup src/mod.tsx",
"check-types": "tsc --noEmit"
},
"devDependencies": {
"rivetkit": "workspace:*",
"tsup": "^8.4.0",
"typescript": "^5.5.2"
},
"stableVersion": "0.8.0",
"dependencies": {
"@floating-ui/react": "^0.27.16",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"motion": "^12.23.25",
"react": "^19.2.1",
"react-dom": "^19.2.1"
}
}
179 changes: 179 additions & 0 deletions rivetkit-typescript/packages/devtools/src/components/DevButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
arrow,
flip,
offset,
shift,
useFloating,
useHover,
useInteractions,
} from "@floating-ui/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
getCornerButtonStyle,
getCornerFromPosition,
useCornerPosition,
} from "../hooks/useCornerPosition";
import { useDraggable } from "../hooks/useDraggable";

const INDICATOR_PADDING = 20;
const STORAGE_KEY = "__rivetkit-devtools";

interface DevButtonProps {
children: React.ReactNode;
onClick?: () => void;
}

export function DevButton({ children, onClick }: DevButtonProps) {
const [isOpen, setIsOpen] = useState(false);
const arrowRef = useRef(null);

const { refs, floatingStyles, context, middlewareData } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: "top",
middleware: [
offset(10),
flip(),
shift({ padding: 8 }),
arrow({ element: arrowRef }),
],
});

const hover = useHover(context, {
delay: { open: 300, close: 0 },
});

const { getReferenceProps, getFloatingProps } = useInteractions([hover]);

const { updateCorner, isRightSide, isBottom } = useCornerPosition({
storageKey: STORAGE_KEY,
defaultCorner: "bottom-right",
});

const handleBeforeSnap = useCallback((x: number, y: number) => {
const newCorner = getCornerFromPosition(x, y);
const isRightSide = newCorner.endsWith("right");
const isBottom = newCorner.startsWith("bottom");

return {
...(isBottom
? { bottom: INDICATOR_PADDING }
: { top: INDICATOR_PADDING }),
...(isRightSide
? { right: INDICATOR_PADDING }
: { left: INDICATOR_PADDING }),
};
}, []);

const handleDragEnd = useCallback(
(x: number, y: number) => {
const newCorner = getCornerFromPosition(x, y);
updateCorner(newCorner);
},
[updateCorner],
);

const {
ref: dragRef,
isDragging,
hasDragged,
handlers,
} = useDraggable<HTMLButtonElement>({
onBeforeSnap: handleBeforeSnap,
onDragEnd: handleDragEnd,
});

const buttonStyle = getCornerButtonStyle({
isBottom,
isRightSide,
isDragging,
padding: INDICATOR_PADDING,
});

// Merge refs for draggable and floating UI
const setRefs = useCallback(
(node: HTMLButtonElement | null) => {
dragRef.current = node;
refs.setReference(node);
},
[refs],
);

// Close tooltip when dragging starts
useEffect(() => {
if (isDragging && isOpen) {
setIsOpen(false);
}
}, [isDragging, isOpen]);

const arrowX = middlewareData.arrow?.x;
const arrowY = middlewareData.arrow?.y;

// Get reference props but don't spread all of them to avoid conflicts
const referenceProps = getReferenceProps();

// Determine arrow side based on actual placement (after flip)
const { placement } = context;
const arrowSide = placement.split("-")[0];

const arrowStyle = useMemo((): React.CSSProperties => {
const style: React.CSSProperties = {
left: arrowX != null ? `${arrowX}px` : "",
top: arrowY != null ? `${arrowY}px` : "",
};

// Position arrow on the correct side
if (arrowSide === "top") {
style.bottom = "-4px";
} else if (arrowSide === "bottom") {
style.top = "-4px";
} else if (arrowSide === "left") {
style.right = "-4px";
} else if (arrowSide === "right") {
style.left = "-4px";
}

return style;
}, [arrowX, arrowY, arrowSide]);

const handleClick = useCallback(() => {
if (!hasDragged && onClick) {
onClick();
}
}, [hasDragged, onClick]);

return (
<>
<button
ref={setRefs}
type="button"
{...handlers}
onClick={handleClick}
onMouseEnter={
referenceProps.onMouseEnter as React.MouseEventHandler<HTMLButtonElement>
}
onMouseLeave={
referenceProps.onMouseLeave as React.MouseEventHandler<HTMLButtonElement>
}
style={buttonStyle}
>
{children}
</button>
{isOpen && !isDragging && (
<div
ref={refs.setFloating}
className="tooltip-container"
style={floatingStyles}
{...getFloatingProps()}
>
<div className="tooltip">Open Inspector</div>
<div
ref={arrowRef}
className="tooltip-arrow"
style={arrowStyle}
/>
</div>
)}
</>
);
}
9 changes: 9 additions & 0 deletions rivetkit-typescript/packages/devtools/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module "*.css" {
const content: string;
export default content;
}

declare module "*.svg" {
const content: string;
export default content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useState } from "react";

type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";

interface UseCornerPositionOptions {
storageKey: string;
defaultCorner?: Corner;
}

export function useCornerPosition(options: UseCornerPositionOptions) {
const { storageKey, defaultCorner = "bottom-right" } = options;
const [corner, setCorner] = useState<Corner>(defaultCorner);

useEffect(() => {
const saved = localStorage.getItem(storageKey);
if (saved) {
setCorner(saved as Corner);
}
}, [storageKey]);

const updateCorner = (newCorner: Corner) => {
setCorner(newCorner);
localStorage.setItem(storageKey, newCorner);
};

const isRightSide = corner.endsWith("right");
const isBottom = corner.startsWith("bottom");

return {
corner,
updateCorner,
isRightSide,
isBottom,
};
}

interface CornerButtonStyleOptions {
isBottom: boolean;
isRightSide: boolean;
isDragging: boolean;
padding?: number;
paddingVertical?: number;
paddingHorizontal?: number;
}

export function getCornerButtonStyle(
options: CornerButtonStyleOptions,
): React.CSSProperties {
const { isBottom, isRightSide, isDragging, padding = 20, paddingVertical, paddingHorizontal } = options;

const verticalPadding = paddingVertical ?? padding;
const horizontalPadding = paddingHorizontal ?? padding;

return {
position: "fixed",
...(isBottom ? { bottom: verticalPadding } : { top: verticalPadding }),
...(isRightSide ? { right: horizontalPadding } : { left: horizontalPadding }),
transform: isDragging
? "translate(var(--drag-x, 0px), var(--drag-y, 0px))"
: undefined,
cursor: isDragging ? "grabbing" : "pointer",
flexDirection: isRightSide ? "row-reverse" : "row",
};
}

export function getCornerFromPosition(x: number, y: number): Corner {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;

if (x < centerX && y < centerY) {
return "top-left";
}
if (x >= centerX && y < centerY) {
return "top-right";
}
if (x < centerX && y >= centerY) {
return "bottom-left";
}
return "bottom-right";
}
Loading
Loading