mirror of
https://github.com/BloopAI/vibe-kanban-web-companion.git
synced 2026-08-23 11:58:32 +00:00
Working ClickToComponent
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"js/ts.implicitProjectConfig.checkJs": true
|
||||
}
|
||||
4880
apps/next/package-lock.json
generated
4880
apps/next/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next",
|
||||
"private": true,
|
||||
"name": "next",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -8,16 +8,16 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"click-to-component": "link:../../packages/click-to-component",
|
||||
"next": "12.1.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
"next": "12.1.5",
|
||||
"react": "latest",
|
||||
"react-dom": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.21",
|
||||
"@types/react": "17.0.40",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-config-next": "12.1.0",
|
||||
"typescript": "4.6.2"
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "18.0.3",
|
||||
"click-to-react-component": "workspace:^",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-config-next": "12.1.5",
|
||||
"typescript": "4.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import '../styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { useClickToComponent } from 'click-to-component'
|
||||
import { ClickToComponent } from 'click-to-react-component'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
useClickToComponent()
|
||||
|
||||
return <Component {...pageProps} />
|
||||
return (
|
||||
<>
|
||||
<ClickToComponent editor="vscode-insiders" />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"env": "env",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"lint": "turbo run lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^1.1.6"
|
||||
"eslint": "8.13.0",
|
||||
"turbo": "^1.2.2"
|
||||
},
|
||||
"packageManager": "pnpm@6.32.3"
|
||||
"packageManager": "pnpm@6.32.3",
|
||||
"dependencies": {
|
||||
"unbuild": "^0.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "click-to-component",
|
||||
"version": "1.0.0",
|
||||
"description": "Option+Click your React/Vue components in your browser to open the source file in VS Code",
|
||||
"source": "src/index.ts",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ericclemmons/click-to-component.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"vue",
|
||||
"dx",
|
||||
"vscode",
|
||||
"devtools"
|
||||
],
|
||||
"author": "Eric Clemmons <eric@smarterspam.com>",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ericclemmons/click-to-component/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ericclemmons/click-to-component#readme",
|
||||
"devDependencies": {
|
||||
"tsup": "^5.12.1"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function useClickToComponent() {
|
||||
console.log("Howdy");
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig((options) => ({
|
||||
clean: !options.watch,
|
||||
// https://tsup.egoist.sh/#generate-declaration-file
|
||||
dts: true,
|
||||
// https://tsup.egoist.sh/#typescript--javascript
|
||||
entry: ["src/**/*.ts"],
|
||||
// https://tsup.egoist.sh/#bundle-formats
|
||||
format: ["cjs", "esm"],
|
||||
// https://tsup.egoist.sh/#metafile
|
||||
metafile: !!options.watch,
|
||||
// https://tsup.egoist.sh/#minify-output
|
||||
minify: !options.watch,
|
||||
// https://tsup.egoist.sh/#bundle-formats
|
||||
legacyOutput: true,
|
||||
// https://tsup.egoist.sh/#generate-sourcemap-file
|
||||
sourcemap: "inline",
|
||||
}));
|
||||
3
packages/click-to-react-component/.eslintrc.json
Normal file
3
packages/click-to-react-component/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "react-app"
|
||||
}
|
||||
1
packages/click-to-react-component/index.js
Normal file
1
packages/click-to-react-component/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { ClickToComponent } from './src/ClickToComponent'
|
||||
8
packages/click-to-react-component/jsconfig.json
Normal file
8
packages/click-to-react-component/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"noImplicitThis": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
45
packages/click-to-react-component/package.json
Normal file
45
packages/click-to-react-component/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "click-to-react-component",
|
||||
"version": "1.0.0",
|
||||
"description": "Option+Click your React components in your browser to open the source file in VS Code",
|
||||
"types": "types.d.ts",
|
||||
"scripts": {
|
||||
"build": "echo \"Fuck build tools\"",
|
||||
"dev": "pnpm run build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ericclemmons/click-to-component.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"vue",
|
||||
"dx",
|
||||
"vscode",
|
||||
"devtools"
|
||||
],
|
||||
"author": "Eric Clemmons <eric@smarterspam.com>",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ericclemmons/click-to-component/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ericclemmons/click-to-component#readme",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||
"@radix-ui/react-icons": "^1.1.0",
|
||||
"@stitches/react": "^1.2.7",
|
||||
"htm": "^3.1.0",
|
||||
"react-popper": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.5",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-react-app": "^7.0.1"
|
||||
}
|
||||
}
|
||||
199
packages/click-to-react-component/src/ClickToComponent.js
Normal file
199
packages/click-to-react-component/src/ClickToComponent.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @typedef {import('../types').ClickToComponentProps} Props
|
||||
*/
|
||||
|
||||
import { html } from 'htm/react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { ContextMenu } from './ContextMenu'
|
||||
import { getSourceForElement } from './getSourceForElement'
|
||||
import { getPathToSource } from './getPathToSource'
|
||||
|
||||
export const State = /** @type {const} */ ({
|
||||
IDLE: 'IDLE',
|
||||
HOVER: 'HOVER',
|
||||
SELECT: 'SELECT',
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {Props} props
|
||||
*/
|
||||
export function ClickToComponent({ editor }) {
|
||||
const [state, setState] = React.useState(
|
||||
/** @type {State[keyof State]} */
|
||||
(State.IDLE)
|
||||
)
|
||||
|
||||
const [target, setTarget] = React.useState(
|
||||
/** @type {HTMLElement | null} */
|
||||
(null)
|
||||
)
|
||||
|
||||
const onClick = React.useCallback(
|
||||
function handleClick(
|
||||
/**
|
||||
* @type {MouseEvent}
|
||||
*/
|
||||
event
|
||||
) {
|
||||
if (state === State.HOVER && target instanceof HTMLElement) {
|
||||
const source = getSourceForElement(target)
|
||||
const path = getPathToSource(source)
|
||||
const url = `${editor}://file/${path}`
|
||||
|
||||
event.preventDefault()
|
||||
window.open(url)
|
||||
|
||||
setState(State.IDLE)
|
||||
}
|
||||
},
|
||||
[editor, state, target]
|
||||
)
|
||||
|
||||
const onClose = React.useCallback(
|
||||
function handleClose(event, returnValue) {
|
||||
if (returnValue) {
|
||||
const url = `${editor}://file/${returnValue}`
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
setState(State.IDLE)
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const onContextMenu = React.useCallback(
|
||||
function handleContextMenu(
|
||||
/**
|
||||
* @type {MouseEvent}
|
||||
*/
|
||||
event
|
||||
) {
|
||||
if (state === State.HOVER && target instanceof HTMLElement) {
|
||||
event.preventDefault()
|
||||
setState(State.SELECT)
|
||||
}
|
||||
},
|
||||
[state, target]
|
||||
)
|
||||
|
||||
const onKeyDown = React.useCallback(
|
||||
function handleKeyDown(
|
||||
/**
|
||||
* @type {KeyboardEvent}
|
||||
*/
|
||||
event
|
||||
) {
|
||||
switch (state) {
|
||||
case State.IDLE:
|
||||
if (event.altKey) setState(State.HOVER)
|
||||
break
|
||||
|
||||
default:
|
||||
}
|
||||
},
|
||||
[state]
|
||||
)
|
||||
|
||||
const onKeyUp = React.useCallback(
|
||||
function handleKeyUp(
|
||||
/**
|
||||
* @type {KeyboardEvent}
|
||||
*/
|
||||
event
|
||||
) {
|
||||
switch (state) {
|
||||
case State.HOVER:
|
||||
setState(State.IDLE)
|
||||
break
|
||||
|
||||
default:
|
||||
}
|
||||
},
|
||||
[state]
|
||||
)
|
||||
|
||||
const onMouseMove = React.useCallback(
|
||||
function handleMouseMove(
|
||||
/** @type {MouseEvent} */
|
||||
event
|
||||
) {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case State.IDLE:
|
||||
case State.HOVER:
|
||||
setTarget(event.target)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[state]
|
||||
)
|
||||
|
||||
React.useEffect(
|
||||
function toggleIndicator() {
|
||||
for (const element of Array.from(
|
||||
document.querySelectorAll('[data-click-to-component]')
|
||||
)) {
|
||||
if (element instanceof HTMLElement) {
|
||||
delete element.dataset.clickToComponent
|
||||
}
|
||||
}
|
||||
|
||||
if (state === State.IDLE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (target instanceof HTMLElement) {
|
||||
target.dataset.clickToComponent = state
|
||||
}
|
||||
},
|
||||
[state, target]
|
||||
)
|
||||
|
||||
React.useEffect(
|
||||
function addEventListenersToWindow() {
|
||||
window.addEventListener('click', onClick, { capture: true })
|
||||
window.addEventListener('contextmenu', onContextMenu, { capture: true })
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('keyup', onKeyUp)
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
|
||||
return function removeEventListenersFromWindow() {
|
||||
window.removeEventListener('click', onClick, { capture: true })
|
||||
window.removeEventListener('contextmenu', onContextMenu, {
|
||||
capture: true,
|
||||
})
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
window.removeEventListener('keyup', onKeyUp)
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
},
|
||||
[onClick, onContextMenu, onKeyDown, onKeyUp, onMouseMove]
|
||||
)
|
||||
|
||||
return html`
|
||||
<style key="click-to-component-style">
|
||||
[data-click-to-component] {
|
||||
cursor: var(--click-to-component-cursor, context-menu) !important;
|
||||
outline: var(
|
||||
--click-to-component-outline,
|
||||
2px solid lightgreen
|
||||
) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
${state === State.SELECT
|
||||
? html`<${ContextMenu}
|
||||
key="click-to-component-contextmenu"
|
||||
onClose=${onClose}
|
||||
target=${target}
|
||||
/>`
|
||||
: null}
|
||||
`
|
||||
}
|
||||
283
packages/click-to-react-component/src/ContextMenu.js
Normal file
283
packages/click-to-react-component/src/ContextMenu.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* @typedef {import('react').DialogHTMLAttributes} DialogHTMLAttributes
|
||||
* @typedef {import('react').HTMLAttributes} HTMLAttributes
|
||||
* @typedef {import('react').MouseEvent<HTMLElement, MouseEvent>} ReactMouseEvent}
|
||||
*/
|
||||
|
||||
import { html } from 'htm/react'
|
||||
import * as React from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
|
||||
import { getDisplayNameForInstance } from './getDisplayNameFromReactInstance'
|
||||
import { getPathToSource } from './getPathToSource'
|
||||
import { getReactInstancesForElement } from './getReactInstancesForElement'
|
||||
import { getSourceForInstance } from './getSourceForInstance'
|
||||
import { getPropsForInstance } from './getPropsForInstance'
|
||||
|
||||
export function ContextMenu({ onClose, target }) {
|
||||
const [contextMenuElement, setContextMenuElement] = React.useState(
|
||||
/** {HTMLElement | null} */
|
||||
null
|
||||
)
|
||||
|
||||
const [arrowElement, setArrowElement] = React.useState(
|
||||
/** {HTMLElement | null} */
|
||||
null
|
||||
)
|
||||
|
||||
const { styles, attributes } = usePopper(target, contextMenuElement, {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'arrow',
|
||||
options: { element: arrowElement },
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: { offset: [0, 8] },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
React.useEffect(
|
||||
// Careful – this can be buggy in strict mode: https://github.com/facebook/react/issues/24399#issuecomment-1104191934
|
||||
function toggleContextMenu() {
|
||||
if (contextMenuElement) {
|
||||
contextMenuElement.showModal()
|
||||
|
||||
return () => contextMenuElement.close()
|
||||
}
|
||||
},
|
||||
[contextMenuElement]
|
||||
)
|
||||
|
||||
// Workaround for this bullshit – https://github.com/microsoft/typescript-styled-plugin/issues/100
|
||||
const popperProps = /** @type {DialogHTMLAttributes} */ ({
|
||||
// Prevent focusing of elements outside of <dialog>
|
||||
inert: '',
|
||||
// When using `.showModal()`, the backdrop `event.target` is `<dialog>`
|
||||
onClick(
|
||||
/**
|
||||
* @type {ReactMouseEvent}
|
||||
*/
|
||||
event
|
||||
) {
|
||||
if (
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.nodeName === 'DIALOG'
|
||||
) {
|
||||
contextMenuElement.close()
|
||||
}
|
||||
},
|
||||
onClose(event) {
|
||||
onClose?.(event, contextMenuElement.returnValue)
|
||||
},
|
||||
ref: setContextMenuElement,
|
||||
style: styles.popper,
|
||||
...attributes.popper,
|
||||
})
|
||||
|
||||
const arrowProps = /** @type {HTMLAttributes} */ ({
|
||||
ref: setArrowElement,
|
||||
style: styles.arrow,
|
||||
})
|
||||
|
||||
const instances = getReactInstancesForElement(target).filter((instance) =>
|
||||
getSourceForInstance(instance)
|
||||
)
|
||||
|
||||
return html`
|
||||
<style key="click-to-component-dialog-style">
|
||||
#click-to-component-dialog::backdrop {
|
||||
background: transparent;
|
||||
transition: backdrop-filter 0.5s ease;
|
||||
}
|
||||
|
||||
#click-to-component-dialog,
|
||||
#click-to-component-dialog * {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
#click-to-component-dialog {
|
||||
background: white;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
overflow: visible;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
|
||||
--shadow-color: 0deg 0% 0%;
|
||||
--shadow-elevation-low: 0px -1px 0.8px hsl(var(--shadow-color) / 0.1),
|
||||
0px -1.2px 0.9px -2.5px hsl(var(--shadow-color) / 0.07),
|
||||
0px -3px 2.3px -5px hsl(var(--shadow-color) / 0.03);
|
||||
|
||||
--shadow-elevation-medium: 0px 1px 0.8px hsl(var(--shadow-color) / 0.11),
|
||||
0px 1.5px 1.1px -1.7px hsl(var(--shadow-color) / 0.08),
|
||||
0px 5.1px 3.8px -3.3px hsl(var(--shadow-color) / 0.05),
|
||||
0px 15px 11.3px -5px hsl(var(--shadow-color) / 0.03);
|
||||
--shadow-elevation-high: 0px 1px 0.8px hsl(var(--shadow-color) / 0.1),
|
||||
0px 1.1px 0.8px -0.7px hsl(var(--shadow-color) / 0.09),
|
||||
0px 2.1px 1.6px -1.4px hsl(var(--shadow-color) / 0.07),
|
||||
0px 4.9px 3.7px -2.1px hsl(var(--shadow-color) / 0.06),
|
||||
0px 10.1px 7.6px -2.9px hsl(var(--shadow-color) / 0.05),
|
||||
0px 18.9px 14.2px -3.6px hsl(var(--shadow-color) / 0.04),
|
||||
0px 31.9px 23.9px -4.3px hsl(var(--shadow-color) / 0.02),
|
||||
0px 50px 37.5px -5px hsl(var(--shadow-color) / 0.01);
|
||||
|
||||
box-shadow: var(--shadow-elevation-high);
|
||||
filter: drop-shadow(0px 0px 0.5px rgba(0 0 0 / 50%));
|
||||
}
|
||||
|
||||
#click-to-component-dialog ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#click-to-component-dialog ol > li {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog ol > * + * {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog button {
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog button:hover {
|
||||
cursor: pointer;
|
||||
filter: invert(1) hue-rotate(180deg) contrast(0.9);
|
||||
}
|
||||
|
||||
#click-to-component-dialog button > * + * {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog button code {
|
||||
color: rgb(87, 70, 175);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
#click-to-component-dialog button code var {
|
||||
background: rgba(0 0 0 / 5%);
|
||||
cursor: help;
|
||||
border-radius: 3px;
|
||||
padding: 3px 6px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
#click-to-component-dialog button cite {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
opacity: 0.5;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
#click-to-component-dialog button cite data::after {
|
||||
content: attr(value);
|
||||
float: right;
|
||||
padding-left: 15px;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
#click-to-component-dialog-arrow,
|
||||
#click-to-component-dialog-arrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
#click-to-component-dialog-arrow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#click-to-component-dialog-arrow::before {
|
||||
visibility: visible;
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
#click-to-component-dialog[data-popper-placement^='top']
|
||||
> #click-to-component-dialog-arrow {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog[data-popper-placement^='bottom']
|
||||
> #click-to-component-dialog-arrow {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog[data-popper-placement^='left']
|
||||
> #click-to-component-dialog-arrow {
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
#click-to-component-dialog[data-popper-placement^='right']
|
||||
> #click-to-component-dialog-arrow {
|
||||
left: -4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<dialog
|
||||
id="click-to-component-dialog"
|
||||
key="click-to-component-dialog"
|
||||
...${popperProps}
|
||||
>
|
||||
<form method="dialog">
|
||||
<ol>
|
||||
${instances.map((instance, i) => {
|
||||
const name = getDisplayNameForInstance(instance)
|
||||
const source = getSourceForInstance(instance)
|
||||
const path = getPathToSource(source)
|
||||
const props = getPropsForInstance(instance)
|
||||
|
||||
return html`
|
||||
<li key=${i}>
|
||||
<button value=${path}>
|
||||
<code>
|
||||
${'<'}${name}
|
||||
${Object.entries(props).map(
|
||||
([prop, value]) => html`
|
||||
${' '}
|
||||
<var key=${prop} title="${value}">${prop}</var>
|
||||
`
|
||||
)}
|
||||
${'>'}
|
||||
</code>
|
||||
<cite>
|
||||
<data value="${source.lineNumber}:${source.columnNumber}">
|
||||
${source.fileName}
|
||||
</data>
|
||||
</cite>
|
||||
</button>
|
||||
</li>
|
||||
`
|
||||
})}
|
||||
</ol>
|
||||
</form>
|
||||
<div id="click-to-component-dialog-arrow" ...${arrowProps} />
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Fiber} Fiber
|
||||
* @param {Fiber} instance
|
||||
*/
|
||||
export function getDisplayNameForInstance(instance) {
|
||||
const { elementType, tag } = instance
|
||||
|
||||
switch (tag) {
|
||||
case 0: // FunctionComponent
|
||||
case 1: // ClassComponent
|
||||
return (
|
||||
elementType.displayName || elementType.name || 'Anonymous Component'
|
||||
)
|
||||
|
||||
case 5: // HostComponent:
|
||||
return elementType
|
||||
|
||||
case 6: // HostText:
|
||||
return 'String'
|
||||
|
||||
case 7: // Fragment
|
||||
return 'React.Fragment'
|
||||
|
||||
case 9: // ContextConsumer
|
||||
return 'Context.Consumer'
|
||||
|
||||
case 10: // ContextProvider
|
||||
return 'Context.Provider'
|
||||
|
||||
case 11: // ForwardRef
|
||||
return 'React.forwardRef'
|
||||
|
||||
case 15: // MemoComponent
|
||||
// Attempt to get name from wrapped component
|
||||
return elementType.type.name || 'React.memo'
|
||||
|
||||
case 16: // LazyComponent
|
||||
return 'React.lazy'
|
||||
|
||||
default:
|
||||
console.warn(`Unrecognized React Fiber tag: ${tag}`, instance)
|
||||
return 'Unknown Component'
|
||||
}
|
||||
}
|
||||
18
packages/click-to-react-component/src/getPathToSource.js
Normal file
18
packages/click-to-react-component/src/getPathToSource.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Source} Source
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Source} source
|
||||
*/
|
||||
export function getPathToSource(source) {
|
||||
const {
|
||||
// It _does_ exist!
|
||||
// @ts-ignore Property 'columnNumber' does not exist on type 'Source'.ts(2339)
|
||||
columnNumber = 1,
|
||||
fileName,
|
||||
lineNumber = 1,
|
||||
} = source
|
||||
|
||||
return `${fileName}:${lineNumber}:${columnNumber}`
|
||||
}
|
||||
38
packages/click-to-react-component/src/getPropsForInstance.js
Normal file
38
packages/click-to-react-component/src/getPropsForInstance.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Fiber} Fiber
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Fiber} instance
|
||||
*/
|
||||
|
||||
export function getPropsForInstance(instance) {
|
||||
/**
|
||||
* @type {Fiber['pendingProps']}
|
||||
*/
|
||||
const props = {}
|
||||
|
||||
console.log({ instance })
|
||||
|
||||
Object.entries(instance.memoizedProps).forEach(([key, value]) => {
|
||||
const type = typeof value
|
||||
|
||||
// Ignore some values, even if they're scalar, because they're not unique enough
|
||||
if (['key'].includes(key) || value === instance.type.defaultProps?.[key]) {
|
||||
return
|
||||
}
|
||||
|
||||
// Scalar values
|
||||
if (
|
||||
['string', 'number', 'boolean', 'symbol'].includes(type) ||
|
||||
value instanceof String ||
|
||||
value instanceof Number ||
|
||||
value instanceof Boolean ||
|
||||
value instanceof Symbol
|
||||
) {
|
||||
props[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
return props
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
export function getReactInstanceForElement(element) {
|
||||
// Prefer React DevTools, which has direct access to `react-dom` for mapping `element` <=> Fiber
|
||||
if ('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window) {
|
||||
// @ts-expect-error - TS2339 - Property '__REACT_DEVTOOLS_GLOBAL_HOOK__' does not exist on type 'Window & typeof globalThis'.
|
||||
const { renderers } = window.__REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
|
||||
for (const renderer of renderers.values()) {
|
||||
try {
|
||||
const fiber = renderer.findFiberByHostInstance(element)
|
||||
|
||||
if (fiber) {
|
||||
return fiber
|
||||
}
|
||||
} catch (e) {
|
||||
// If React is mid-render, references to previous nodes may disappear during the click events
|
||||
// (This is especially true for interactive elements, like menus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ('_reactRootContainer' in element) {
|
||||
// @ts-expect-error - TS2339 - Property '_reactRootContainer' does not exist on type 'HTMLElement'.
|
||||
return element._reactRootContainer._internalRoot.current.child
|
||||
}
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in element) {
|
||||
// Pre-Fiber access React internals
|
||||
if (key.startsWith('__reactInternalInstance$')) {
|
||||
return element[key]
|
||||
}
|
||||
|
||||
// Fiber access to React internals
|
||||
if (key.startsWith('__reactFiber')) {
|
||||
return element[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Fiber} Fiber
|
||||
*/
|
||||
|
||||
import { getReactInstanceForElement } from './getReactInstanceForElement'
|
||||
|
||||
export function getReactInstancesForElement(
|
||||
/** @type {HTMLElement} */
|
||||
element
|
||||
) {
|
||||
/** @type {Set<Fiber>} */
|
||||
const instances = new Set()
|
||||
let instance = getReactInstanceForElement(element)
|
||||
|
||||
while (instance) {
|
||||
instances.add(instance)
|
||||
|
||||
instance = instance._debugOwner
|
||||
}
|
||||
|
||||
return Array.from(instances)
|
||||
}
|
||||
20
packages/click-to-react-component/src/getSourceForElement.js
Normal file
20
packages/click-to-react-component/src/getSourceForElement.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getReactInstanceForElement } from './getReactInstanceForElement'
|
||||
import { getSourceForInstance } from './getSourceForInstance'
|
||||
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Fiber} Fiber
|
||||
*/
|
||||
|
||||
export function getSourceForElement(
|
||||
/**
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
element
|
||||
) {
|
||||
const instance = getReactInstanceForElement(element)
|
||||
const source = getSourceForInstance(instance)
|
||||
|
||||
if (source) return source
|
||||
|
||||
console.warn("Couldn't find a React instance for the element", element)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @typedef {import('react-reconciler').Fiber} Fiber
|
||||
* @typedef {import('react-reconciler').Source} Source
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Fiber} instance
|
||||
*/
|
||||
export function getSourceForInstance({ _debugSource }) {
|
||||
if (!_debugSource) return
|
||||
|
||||
const {
|
||||
// It _does_ exist!
|
||||
// @ts-ignore Property 'columnNumber' does not exist on type 'Source'.ts(2339)
|
||||
columnNumber = 1,
|
||||
fileName,
|
||||
lineNumber = 1,
|
||||
} = _debugSource
|
||||
|
||||
return { columnNumber, fileName, lineNumber }
|
||||
}
|
||||
7
packages/click-to-react-component/types.d.ts
vendored
Normal file
7
packages/click-to-react-component/types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ClickToComponent } from './src/ClickToComponent'
|
||||
|
||||
export type Editor = 'vscode' | 'vscode-insiders'
|
||||
|
||||
export type ClickToComponentProps = {
|
||||
editor?: Editor
|
||||
}
|
||||
3798
pnpm-lock.yaml
generated
3798
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user