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
4 changes: 4 additions & 0 deletions _package-export/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ The component exposes these callback props (added in 1.2.0):

**API stability**: These are public contracts. Changing signatures or removing callbacks is a breaking change requiring a major version bump.

**1.4.0 additions:**
- `sourceAttribute` prop - configurable source detection attribute
- `sourceFile`, `sourceComponent` on Annotation type

**Expansion ideas** (for future consideration):
- `onActivate` / `onDeactivate` - toolbar state changes
- `getAnnotations()` ref method - programmatic access
Expand Down
6 changes: 6 additions & 0 deletions _package-export/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an
- **Area selection** – Drag to annotate any region, even empty space
- **Animation pause** – Freeze CSS animations to capture specific states
- **Structured output** – Copy markdown with selectors, positions, and context
- **Source detection** – Shows file:line for React components (dev mode with TanStack Devtools)
- **Programmatic access** – Callback prop for direct integration with tools
- **Dark/light mode** – Matches your preference or set manually
- **Zero dependencies** – Pure CSS animations, no runtime libraries
Expand All @@ -47,6 +48,7 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an
| `onAnnotationsClear` | `(annotations: Annotation[]) => void` | - | Called when all annotations are cleared |
| `onCopy` | `(markdown: string) => void` | - | Callback with markdown output when copy is clicked |
| `copyToClipboard` | `boolean` | `true` | Set to false to prevent writing to clipboard |
| `sourceAttribute` | `string` | `"data-tsd-source"` | Data attribute to read source file path from |

### Programmatic Integration

Expand Down Expand Up @@ -102,6 +104,10 @@ type Annotation = {
accessibility?: string;
isMultiSelect?: boolean;
isFixed?: boolean;

// Source location (dev mode only)
sourceFile?: string; // e.g., "src/components/Button.tsx:42"
sourceComponent?: string; // Component name if available
};
```

Expand Down
52 changes: 52 additions & 0 deletions _package-export/src/components/page-toolbar-css/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ import {
saveAnnotations,
getStorageKey,
} from "../../utils/storage";
import {
getSourceLocation,
formatSourceLocation,
DEFAULT_SOURCE_ATTRIBUTE,
} from "../../utils/source-location";

import type { Annotation } from "../../types";
import styles from "./styles.module.scss";
Expand Down Expand Up @@ -179,6 +184,13 @@ function generateOutput(
if (a.isMultiSelect && a.fullPath) {
output += `*Forensic data shown for first element of selection*\n`;
}
if (a.sourceFile) {
output += `**Source:** ${a.sourceFile}`;
if (a.sourceComponent) {
output += ` (component: ${a.sourceComponent})`;
}
output += `\n`;
}
if (a.fullPath) {
output += `**Full DOM Path:** ${a.fullPath}\n`;
}
Expand Down Expand Up @@ -208,6 +220,13 @@ function generateOutput(
} else {
// Standard and detailed modes
output += `### ${i + 1}. ${a.element}\n`;
if (a.sourceFile) {
output += `**Source:** ${a.sourceFile}`;
if (a.sourceComponent) {
output += ` (${a.sourceComponent})`;
}
output += `\n`;
}
output += `**Location:** ${a.elementPath}\n`;

if (detailLevel === "detailed") {
Expand Down Expand Up @@ -261,6 +280,8 @@ export type PageFeedbackToolbarCSSProps = {
onCopy?: (markdown: string) => void;
/** Whether to copy to clipboard when the copy button is clicked. Defaults to true. */
copyToClipboard?: boolean;
/** Data attribute name to read source location from. Defaults to "data-tsd-source" (TanStack Devtools). */
sourceAttribute?: string;
};

/** Alias for PageFeedbackToolbarCSSProps */
Expand All @@ -280,6 +301,7 @@ export function PageFeedbackToolbarCSS({
onAnnotationsClear,
onCopy,
copyToClipboard = true,
sourceAttribute = DEFAULT_SOURCE_ATTRIBUTE,
}: PageFeedbackToolbarCSSProps = {}) {
const [isActive, setIsActive] = useState(false);
const [annotations, setAnnotations] = useState<Annotation[]>([]);
Expand Down Expand Up @@ -307,6 +329,8 @@ export function PageFeedbackToolbarCSS({
computedStyles?: string;
computedStylesObj?: Record<string, string>;
nearbyElements?: string;
sourceFile?: string;
sourceComponent?: string;
} | null>(null);
const [copied, setCopied] = useState(false);
const [cleared, setCleared] = useState(false);
Expand Down Expand Up @@ -499,6 +523,7 @@ export function PageFeedbackToolbarCSS({

const rect = element.getBoundingClientRect();
const { name, path } = identifyElement(element);
const sourceResult = getSourceLocation(element, sourceAttribute);

const newAnnotation: Annotation = {
id: `demo-${Date.now()}-${index}`,
Expand All @@ -517,6 +542,11 @@ export function PageFeedbackToolbarCSS({
},
nearbyText: getNearbyText(element),
cssClasses: getElementClasses(element),
sourceFile:
sourceResult.found && sourceResult.source
? formatSourceLocation(sourceResult.source, "path")
: undefined,
sourceComponent: sourceResult.source?.componentName,
};

setAnnotations((prev) => [...prev, newAnnotation]);
Expand Down Expand Up @@ -764,6 +794,14 @@ export function PageFeedbackToolbarCSS({
const computedStylesObj = getDetailedComputedStyles(elementUnder);
const computedStylesStr = getForensicComputedStyles(elementUnder);

// Capture source location for React components
const sourceResult = getSourceLocation(elementUnder, sourceAttribute);
const sourceFile =
sourceResult.found && sourceResult.source
? formatSourceLocation(sourceResult.source, "path")
: undefined;
const sourceComponent = sourceResult.source?.componentName;

setPendingAnnotation({
x,
y,
Expand All @@ -785,6 +823,8 @@ export function PageFeedbackToolbarCSS({
computedStyles: computedStylesStr,
computedStylesObj,
nearbyElements: getNearbyElements(elementUnder),
sourceFile,
sourceComponent,
});
setHoverInfo(null);
};
Expand Down Expand Up @@ -1163,6 +1203,14 @@ export function PageFeedbackToolbarCSS({
getDetailedComputedStyles(firstElement);
const firstElementComputedStylesStr = getForensicComputedStyles(firstElement);

// Capture source location from first element
const sourceResult = getSourceLocation(firstElement, sourceAttribute);
const sourceFile =
sourceResult.found && sourceResult.source
? formatSourceLocation(sourceResult.source, "path")
: undefined;
const sourceComponent = sourceResult.source?.componentName;

setPendingAnnotation({
x,
y,
Expand All @@ -1184,6 +1232,8 @@ export function PageFeedbackToolbarCSS({
nearbyElements: getNearbyElements(firstElement),
cssClasses: getElementClasses(firstElement),
nearbyText: getNearbyText(firstElement),
sourceFile,
sourceComponent,
});
} else {
// No elements selected, but allow annotation on empty area
Expand Down Expand Up @@ -1249,6 +1299,8 @@ export function PageFeedbackToolbarCSS({
accessibility: pendingAnnotation.accessibility,
computedStyles: pendingAnnotation.computedStyles,
nearbyElements: pendingAnnotation.nearbyElements,
sourceFile: pendingAnnotation.sourceFile,
sourceComponent: pendingAnnotation.sourceComponent,
};

setAnnotations((prev) => [...prev, newAnnotation]);
Expand Down
4 changes: 4 additions & 0 deletions _package-export/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type Annotation = {
accessibility?: string;
isMultiSelect?: boolean; // true if created via drag selection
isFixed?: boolean; // true if element has fixed/sticky positioning (marker stays fixed)
/** Source file path with line number (dev mode only). e.g., "src/components/Button.tsx:42" */
sourceFile?: string;
/** React component name. e.g., "Button" */
sourceComponent?: string;
};

// TODO: Add configuration types when abstracting config
Expand Down
49 changes: 38 additions & 11 deletions _package-export/src/utils/source-location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,25 +375,52 @@ function findDebugSourceReact19(
}

/**
* Gets the source file location for a DOM element in a React application
* Default data attribute for source location detection.
* TanStack Devtools uses "data-tsd-source" by default.
*/
export const DEFAULT_SOURCE_ATTRIBUTE = "data-tsd-source";

/**
* Gets the source file location for a DOM element in a React application.
*
* This function attempts to extract the source file path and line number
* where a React component is defined. This only works in development mode
* as production builds strip debug information.
*
* @param element - DOM element to get source location for
* @param sourceAttribute - Data attribute to check for source info (default: "data-tsd-source")
* @returns SourceLocationResult with location info or reason for failure
*
* @example
* ```ts
* const result = getSourceLocation(element);
* if (result.found && result.source) {
* console.log(`${result.source.fileName}:${result.source.lineNumber}`);
* // Output: "/src/components/Button.tsx:42"
* }
* ```
*/
export function getSourceLocation(element: HTMLElement): SourceLocationResult {
export function getSourceLocation(
element: HTMLElement,
sourceAttribute: string = DEFAULT_SOURCE_ATTRIBUTE
): SourceLocationResult {
// First, check for source attribute on DOM elements (e.g., data-tsd-source from TanStack Devtools)
// This can be customized via the sourceAttribute parameter
let current: HTMLElement | null = element;
while (current) {
const sourceValue = current.getAttribute(sourceAttribute);
if (sourceValue) {
// Format: "/src/components/Button.tsx:42:5" or "src/components/Button.tsx:42"
const match = sourceValue.match(/^(.+):(\d+)(?::(\d+))?$/);
if (match) {
const [, fileName, lineStr, colStr] = match;
return {
found: true,
source: {
fileName: fileName,
lineNumber: parseInt(lineStr, 10),
columnNumber: colStr ? parseInt(colStr, 10) : undefined,
componentName: undefined,
},
isReactApp: true,
isProduction: false,
};
}
}
current = current.parentElement;
}

// Detect React environment
const reactInfo = detectReactApp();

Expand Down