From 8a0bb7112f0aa5a07aabfa0847e5efbdf91a8127 Mon Sep 17 00:00:00 2001 From: Ben MacLaurin Date: Sat, 28 Feb 2026 21:37:11 -0800 Subject: [PATCH] feat: add commit widget with prompt, spotlight, and text-morph feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a persistent commit widget (Dynamic Island-style) at the bottom of the viewport with: - TextMorph-powered label showing the latest grabbed element (ComponentName.tagName) - Expandable prompt input that copies text to clipboard on submit - Spotlight effect: hides text/SVGs/images on the rest of the page, applies grayscale and a subtle overlay, leaving only the target element in full color - Dynamic Island scale-up animation when prompt opens - Light/dark mode support via prefers-color-scheme - History button with ⌃Z shortcut to copy an undo prompt - Suppresses legacy selection labels and overlay canvas while prompt is open New files: - src/components/commit-widget.tsx (SolidJS component) - src/components/text-morph.tsx (SolidJS wrapper for torph TextMorph class) Modified: - renderer.tsx: wires CommitWidget, hides legacy labels/overlay during prompt - core/index.tsx: adds handleActivateForCopy, passes latestGrabbedElement - types.ts: adds onActivateForCopy, latestGrabbedElement to renderer props - constants.ts: adds COMMIT_WIDGET_FEEDBACK_DURATION_MS, COMMIT_WIDGET_PROMPT_WIDTH_PX - styles.css: adds commit widget styles with light mode overrides - tsup.config.ts: adds torph to noExternal - package.json: adds torph dependency --- packages/react-grab/package.json | 3 +- .../src/components/commit-widget.tsx | 315 ++++++++++++++++++ .../src/components/context-menu.tsx | 5 +- .../react-grab/src/components/renderer.tsx | 89 +---- .../react-grab/src/components/text-morph.tsx | 32 ++ .../src/components/toolbar/index.tsx | 8 +- packages/react-grab/src/constants.ts | 3 + packages/react-grab/src/core/index.tsx | 17 + packages/react-grab/src/styles.css | 159 +++++++++ packages/react-grab/src/types.ts | 3 + .../src/utils/create-menu-highlight.ts | 14 +- packages/react-grab/tsup.config.ts | 4 +- pnpm-lock.yaml | 20 +- 13 files changed, 575 insertions(+), 97 deletions(-) create mode 100644 packages/react-grab/src/components/commit-widget.tsx create mode 100644 packages/react-grab/src/components/text-morph.tsx diff --git a/packages/react-grab/package.json b/packages/react-grab/package.json index b812e7be3..f6e3373f4 100644 --- a/packages/react-grab/package.json +++ b/packages/react-grab/package.json @@ -95,7 +95,8 @@ "dependencies": { "@medv/finder": "^4.0.2", "bippy": "^0.5.30", - "solid-js": "^1.9.10" + "solid-js": "^1.9.10", + "torph": "^0.0.5" }, "devDependencies": { "@babel/core": "^7.28.5", diff --git a/packages/react-grab/src/components/commit-widget.tsx b/packages/react-grab/src/components/commit-widget.tsx new file mode 100644 index 000000000..5434c4e71 --- /dev/null +++ b/packages/react-grab/src/components/commit-widget.tsx @@ -0,0 +1,315 @@ +import { createSignal, createEffect, on, onCleanup } from "solid-js"; +import type { Component } from "solid-js"; +import type { HistoryItem } from "../types.js"; +import { TextMorph } from "./text-morph.js"; +import { + COMMIT_WIDGET_FEEDBACK_DURATION_MS, + COMMIT_WIDGET_PROMPT_WIDTH_PX, + Z_INDEX_HOST, +} from "../constants.js"; + +const UNDO_CLIPBOARD_PROMPT = + "Undo the latest change you just made. Revert the most recent modification to the file(s) you last edited. Do not ask for confirmation — just undo it."; + +const DEFAULT_HISTORY_LABEL = "History"; + +const formatElementLabel = (item: HistoryItem): string => { + if (item.componentName) return `${item.componentName}.${item.tagName}`; + return item.tagName; +}; + +const HIDDEN_MEDIA_TAGS = new Set(["IMG", "SVG", "PICTURE", "VIDEO", "CANVAS"]); + +interface ModifiedElement { + element: HTMLElement | SVGElement; + previousColor: string; + previousTextFillColor: string; + previousTextShadow: string; + previousVisibility: string; + previousFilter: string; + didHideVisibility: boolean; +} + +const isStylableElement = ( + element: Element, +): element is HTMLElement | SVGElement => + element instanceof HTMLElement || element instanceof SVGElement; + +const hideTextInSubtree = ( + element: HTMLElement | SVGElement, + modifiedElements: ModifiedElement[], +) => { + modifiedElements.push({ + element, + previousColor: element.style.color, + previousTextFillColor: element.style.getPropertyValue( + "-webkit-text-fill-color", + ), + previousTextShadow: element.style.textShadow, + previousVisibility: element.style.visibility, + previousFilter: element.style.filter, + didHideVisibility: HIDDEN_MEDIA_TAGS.has(element.tagName), + }); + element.style.color = "transparent"; + element.style.setProperty("-webkit-text-fill-color", "transparent"); + element.style.textShadow = "none"; + element.style.filter = "grayscale(1)"; + if (HIDDEN_MEDIA_TAGS.has(element.tagName)) { + element.style.visibility = "hidden"; + } + + for (const child of Array.from(element.children)) { + if (isStylableElement(child)) { + hideTextInSubtree(child, modifiedElements); + } + } +}; + +const hidePageTextExcept = ( + targetElement: Element, + shadowHost: Element | null, +): ModifiedElement[] => { + const modifiedElements: ModifiedElement[] = []; + let current: Element | null = targetElement; + + while (current && current !== document.body && current !== document.documentElement) { + const parent: HTMLElement | null = current.parentElement; + if (!parent) break; + + for (const sibling of Array.from(parent.children)) { + if (sibling === current || sibling === shadowHost) continue; + if (isStylableElement(sibling)) { + hideTextInSubtree(sibling, modifiedElements); + } + } + + current = parent; + } + + return modifiedElements; +}; + +const restorePageText = (modifiedElements: ModifiedElement[]) => { + for (const { + element, + previousColor, + previousTextFillColor, + previousTextShadow, + previousVisibility, + didHideVisibility, + } of modifiedElements) { + element.style.color = previousColor; + if (previousTextFillColor) { + element.style.setProperty( + "-webkit-text-fill-color", + previousTextFillColor, + ); + } else { + element.style.removeProperty("-webkit-text-fill-color"); + } + element.style.textShadow = previousTextShadow; + element.style.filter = previousFilter; + if (didHideVisibility) { + element.style.visibility = previousVisibility; + } + } +}; + +interface CommitWidgetProps { + copyCount?: number; + historyItems?: HistoryItem[]; + onActivateForCopy?: () => void; + latestGrabbedElement?: Element; + onPromptOpenChange?: (isOpen: boolean) => void; +} + +export const CommitWidget: Component = (props) => { + const [historyLabel, setHistoryLabel] = createSignal(DEFAULT_HISTORY_LABEL); + const [isPromptOpen, setIsPromptOpen] = createSignal(false); + const [promptText, setPromptText] = createSignal(""); + const [promptLabel, setPromptLabel] = createSignal("Prompt"); + + let promptInputRef: HTMLInputElement | undefined; + let widgetRef: HTMLDivElement | undefined; + + createEffect( + on( + () => isPromptOpen(), + (isOpen) => props.onPromptOpenChange?.(isOpen), + ), + ); + + const getShadowHost = (): Element | null => { + if (!widgetRef) return null; + const root = widgetRef.getRootNode(); + if (root instanceof ShadowRoot) return root.host; + return null; + }; + + createEffect(() => { + if (!isPromptOpen() || !props.latestGrabbedElement) return; + + const modifiedElements = hidePageTextExcept( + props.latestGrabbedElement, + getShadowHost(), + ); + + const overlay = document.createElement("div"); + overlay.style.cssText = + "position:fixed;box-shadow:0 0 0 9999px rgba(0,0,0,0.08);z-index:2147483644;pointer-events:none"; + document.body.appendChild(overlay); + + const updateOverlayPosition = () => { + const rect = props.latestGrabbedElement?.getBoundingClientRect(); + if (!rect) return; + overlay.style.top = `${rect.top}px`; + overlay.style.left = `${rect.left}px`; + overlay.style.width = `${rect.width}px`; + overlay.style.height = `${rect.height}px`; + }; + + updateOverlayPosition(); + window.addEventListener("scroll", updateOverlayPosition, true); + window.addEventListener("resize", updateOverlayPosition); + + onCleanup(() => { + restorePageText(modifiedElements); + overlay.remove(); + window.removeEventListener("scroll", updateOverlayPosition, true); + window.removeEventListener("resize", updateOverlayPosition); + }); + }); + + createEffect( + on( + () => props.copyCount ?? 0, + () => { + const latestItem = props.historyItems?.[0]; + if (latestItem) { + setHistoryLabel(formatElementLabel(latestItem)); + } + setIsPromptOpen(true); + requestAnimationFrame(() => promptInputRef?.focus()); + }, + { defer: true }, + ), + ); + + const handlePromptSubmit = () => { + const trimmedPromptText = promptText().trim(); + if (!trimmedPromptText) return; + + void navigator.clipboard.writeText(trimmedPromptText); + setPromptLabel("Copied"); + setPromptText(""); + setTimeout(() => { + setPromptLabel("Prompt"); + setIsPromptOpen(false); + }, COMMIT_WIDGET_FEEDBACK_DURATION_MS); + }; + + const handlePromptButtonClick = () => { + if (isPromptOpen() && promptText().trim()) { + handlePromptSubmit(); + return; + } + + if (isPromptOpen()) { + setIsPromptOpen(false); + setPromptText(""); + return; + } + + props.onActivateForCopy?.(); + }; + + const handleWindowKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.key === "z") { + event.preventDefault(); + void navigator.clipboard.writeText(UNDO_CLIPBOARD_PROMPT); + setHistoryLabel("Prompt copied"); + setTimeout(() => { + const latestItem = props.historyItems?.[0]; + setHistoryLabel( + latestItem ? formatElementLabel(latestItem) : DEFAULT_HISTORY_LABEL, + ); + }, COMMIT_WIDGET_FEEDBACK_DURATION_MS); + } + }; + + window.addEventListener("keydown", handleWindowKeyDown); + onCleanup(() => { + window.removeEventListener("keydown", handleWindowKeyDown); + }); + + const computedPromptLabel = () => { + if (promptLabel() !== "Prompt") return promptLabel(); + if (isPromptOpen() && promptText().trim()) return "Copy"; + return "Prompt"; + }; + + return ( +
+
+
+ {historyLabel()} + {historyLabel() === DEFAULT_HISTORY_LABEL && ( + ⌃Z + )} +
+ +
+ +
+ setPromptText(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + handlePromptSubmit(); + } + if (event.key === "Escape") { + setIsPromptOpen(false); + setPromptText(""); + } + }} + placeholder="Type a prompt..." + class="commit-widget-prompt-input" + /> +
+ + +
+
+ ); +}; diff --git a/packages/react-grab/src/components/context-menu.tsx b/packages/react-grab/src/components/context-menu.tsx index cf47b6078..f0961a65b 100644 --- a/packages/react-grab/src/components/context-menu.tsx +++ b/packages/react-grab/src/components/context-menu.tsx @@ -305,7 +305,10 @@ export const ContextMenu: Component = (props) => { />
-
+
= (props) => { + const [isCommitPromptOpen, setIsCommitPromptOpen] = createSignal(false); + return ( <> + = (props) => { "box-shadow": `inset 0 0 ${FROZEN_GLOW_EDGE_PX}px ${FROZEN_GLOW_COLOR}`, }} /> - = (props) => { )} - - { - if (props.selectionFilePath) { - openFile(props.selectionFilePath, props.selectionLineNumber); - } - }} - isContextMenuOpen={props.contextMenuPosition !== null} - /> - - - - {(instance) => ( - { - const currentInstance = instance(); - const hasCompletedStatus = - currentInstance.status === "copied" || - currentInstance.status === "fading"; - if ( - !hasCompletedStatus || - !isElementConnected(currentInstance.element) - ) { - return undefined; - } - return () => - props.onShowContextMenuInstance?.(currentInstance.id); - })()} - onHoverChange={(isHovered) => - props.onLabelInstanceHoverChange?.(instance().id, isHovered) - } - /> - )} - - = (props) => { onDismiss={props.onHistoryDismiss} onDropdownHover={props.onHistoryDropdownHover} /> + + + + ); }; diff --git a/packages/react-grab/src/components/text-morph.tsx b/packages/react-grab/src/components/text-morph.tsx new file mode 100644 index 000000000..cf6ee1832 --- /dev/null +++ b/packages/react-grab/src/components/text-morph.tsx @@ -0,0 +1,32 @@ +import { onMount, onCleanup, createEffect } from "solid-js"; +import type { Component } from "solid-js"; +import { TextMorph as TorphTextMorph } from "torph"; + +interface TextMorphProps { + children: string; +} + +export const TextMorph: Component = (props) => { + let spanRef: HTMLSpanElement | undefined; + let morphInstance: TorphTextMorph | undefined; + + onMount(() => { + if (!spanRef) return; + morphInstance = new TorphTextMorph({ + element: spanRef, + duration: 300, + }); + morphInstance.update(props.children); + }); + + createEffect(() => { + const text = props.children; + morphInstance?.update(text); + }); + + onCleanup(() => { + morphInstance?.destroy(); + }); + + return ; +}; diff --git a/packages/react-grab/src/components/toolbar/index.tsx b/packages/react-grab/src/components/toolbar/index.tsx index 8a49f2291..727bea26b 100644 --- a/packages/react-grab/src/components/toolbar/index.tsx +++ b/packages/react-grab/src/components/toolbar/index.tsx @@ -1486,9 +1486,7 @@ export const Toolbar: Component = (props) => { "transform-origin": getTransformOrigin(), }} onPointerDown={handlePointerDown} - onMouseEnter={() => - !isCollapsed() && props.onSelectHoverChange?.(true) - } + onMouseEnter={() => !isCollapsed() && props.onSelectHoverChange?.(true)} onMouseLeave={() => props.onSelectHoverChange?.(false)} >
= (props) => { data-react-grab-ignore-events data-react-grab-toolbar-toggle aria-label={ - props.isActive ? "Stop selecting element" : "Select element" + props.isActive + ? "Stop selecting element" + : "Select element" } aria-pressed={Boolean(props.isActive)} class={cn( diff --git a/packages/react-grab/src/constants.ts b/packages/react-grab/src/constants.ts index b5f4026b4..606076c47 100644 --- a/packages/react-grab/src/constants.ts +++ b/packages/react-grab/src/constants.ts @@ -250,3 +250,6 @@ export const RELEVANT_CSS_PROPERTIES = new Set([ "object-fit", "object-position", ]); + +export const COMMIT_WIDGET_FEEDBACK_DURATION_MS = 1500; +export const COMMIT_WIDGET_PROMPT_WIDTH_PX = 192; diff --git a/packages/react-grab/src/core/index.tsx b/packages/react-grab/src/core/index.tsx index 192354ea0..59d14d339 100644 --- a/packages/react-grab/src/core/index.tsx +++ b/packages/react-grab/src/core/index.tsx @@ -152,6 +152,7 @@ import { import { copyContent } from "../utils/copy-content.js"; import { joinSnippets } from "../utils/join-snippets.js"; + const builtInPlugins = [ copyPlugin, commentPlugin, @@ -1729,6 +1730,15 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { } }; + const handleActivateForCopy = () => { + if (!isActivated() && isEnabled()) { + toggleActivate(); + } + }; + + + + const enterCommentModeForElement = ( element: Element, positionX: number, @@ -4034,6 +4044,13 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { handleHistoryClear(); }} onClearHistoryCancel={dismissClearPrompt} + onActivateForCopy={handleActivateForCopy} + latestGrabbedElement={ + historyItems()[0] + ? getFirstConnectedHistoryElement(historyItems()[0]) + : undefined + } + /> ); }, rendererRoot); diff --git a/packages/react-grab/src/styles.css b/packages/react-grab/src/styles.css index 97ea853c6..c2bebf29c 100644 --- a/packages/react-grab/src/styles.css +++ b/packages/react-grab/src/styles.css @@ -302,3 +302,162 @@ min-width: 44px; } } + +/* torph text morph styles (duplicated here for shadow DOM compatibility) */ +[torph-root] { + display: inline-flex; + position: relative; + will-change: width, height; + transition-property: width, height; + white-space: nowrap; +} + +[torph-item] { + display: inline-block; + will-change: opacity, transform; + transform: none; + opacity: 1; +} + +/* commit widget */ +@keyframes commit-widget-enter { + from { + opacity: 0; + transform: translateX(-50%) translateY(8px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +.commit-widget-container { + display: flex; + align-items: center; + gap: 4px; + border-radius: 14px; + border: 0.5px solid color(display-p3 0.143 0.143 0.143); + background: color(display-p3 0.11 0.11 0.11); + padding: 4px; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.2), + 0 4px 8px rgba(0, 0, 0, 0.2), + 0 12px 24px rgba(0, 0, 0, 0.25), + 0 24px 48px rgba(0, 0, 0, 0.3); + animation: commit-widget-enter 200ms ease-out; + transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1); + will-change: transform; +} + +.commit-widget-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + height: 24px; + padding: 0 8px; + border: none; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + cursor: pointer; + user-select: none; + background: transparent; + color: #e5e5e5; + transition: none; +} + +.commit-widget-button-ghost:hover { + background: rgba(255, 255, 255, 0.1); +} + +.commit-widget-kbd { + display: inline-flex; + align-items: center; + justify-content: center; + height: 20px; + min-width: 20px; + padding: 0 4px; + border-radius: 2px; + font-family: inherit; + font-size: 12px; + font-weight: 500; + pointer-events: none; + user-select: none; + background: transparent; + color: #737373; +} + +.commit-widget-divider { + width: 1px; + height: 12px; + background: rgba(255, 255, 255, 0.1); +} + +.commit-widget-button-disabled { + opacity: 0.5; + cursor: default; +} + + +.commit-widget-prompt-input-wrapper { + overflow: hidden; + transition: + width 150ms ease-out, + opacity 150ms ease-out; +} + +.commit-widget-prompt-input { + height: 24px; + width: 192px; + padding: 0 8px; + border: none; + background: transparent; + font-size: 12px; + font-family: inherit; + outline: none; + color: white; +} + +.commit-widget-prompt-input::placeholder { + color: #737373; +} + +@media (prefers-color-scheme: light) { + .commit-widget-container { + border-color: color(display-p3 0.878 0.878 0.878); + background: color(display-p3 0.98 0.98 0.98); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.06), + 0 4px 8px rgba(0, 0, 0, 0.06), + 0 12px 24px rgba(0, 0, 0, 0.08), + 0 24px 48px rgba(0, 0, 0, 0.1); + } + + .commit-widget-button { + color: #1a1a1a; + } + + .commit-widget-button-ghost:hover { + background: rgba(0, 0, 0, 0.06); + } + + .commit-widget-kbd { + color: #a3a3a3; + } + + .commit-widget-divider { + background: rgba(0, 0, 0, 0.1); + } + + + .commit-widget-prompt-input { + color: #1a1a1a; + } + + .commit-widget-prompt-input::placeholder { + color: #a3a3a3; + } + +} diff --git a/packages/react-grab/src/types.ts b/packages/react-grab/src/types.ts index f62392f3c..fc2c75bf5 100644 --- a/packages/react-grab/src/types.ts +++ b/packages/react-grab/src/types.ts @@ -570,6 +570,9 @@ export interface ReactGrabRendererProps { clearPromptPosition?: DropdownAnchor | null; onClearHistoryConfirm?: () => void; onClearHistoryCancel?: () => void; + onActivateForCopy?: () => void; + latestGrabbedElement?: Element; + } export interface GrabbedBox { diff --git a/packages/react-grab/src/utils/create-menu-highlight.ts b/packages/react-grab/src/utils/create-menu-highlight.ts index ecddf3d27..ae38e1b93 100644 --- a/packages/react-grab/src/utils/create-menu-highlight.ts +++ b/packages/react-grab/src/utils/create-menu-highlight.ts @@ -20,12 +20,10 @@ interface MenuHighlightController { const DEFAULT_HIDDEN_OPACITY = "0"; const DEFAULT_VISIBLE_OPACITY = "1"; -export const createAnimatedBoundsFollower = ( - { - hiddenOpacity = DEFAULT_HIDDEN_OPACITY, - visibleOpacity = DEFAULT_VISIBLE_OPACITY, - }: AnimatedBoundsFollowerOptions = {}, -): AnimatedBoundsFollowerController => { +export const createAnimatedBoundsFollower = ({ + hiddenOpacity = DEFAULT_HIDDEN_OPACITY, + visibleOpacity = DEFAULT_VISIBLE_OPACITY, +}: AnimatedBoundsFollowerOptions = {}): AnimatedBoundsFollowerController => { let containerElement: HTMLElement | undefined; let followerElement: HTMLElement | undefined; @@ -34,9 +32,7 @@ export const createAnimatedBoundsFollower = ( followerElement.style.opacity = hiddenOpacity; }; - const followElement = ( - targetElement: HTMLElement | undefined, - ): void => { + const followElement = (targetElement: HTMLElement | undefined): void => { if (!followerElement || !containerElement) return; if (!targetElement) { hideFollower(); diff --git a/packages/react-grab/tsup.config.ts b/packages/react-grab/tsup.config.ts index 34d0049ca..9c074805b 100644 --- a/packages/react-grab/tsup.config.ts +++ b/packages/react-grab/tsup.config.ts @@ -50,7 +50,7 @@ const DEFAULT_OPTIONS: Options = { ".css": "text", }, minify: process.env.NODE_ENV === "production", - noExternal: ["clsx", "tailwind-merge", "solid-js", "bippy"], + noExternal: ["clsx", "tailwind-merge", "solid-js", "bippy", "torph"], onSuccess: process.env.COPY ? "pbcopy < ./dist/index.global.js" : undefined, outDir: "./dist", sourcemap: false, @@ -140,7 +140,7 @@ const reactBuildConfig: Options = { ".css": "text", }, minify: false, - noExternal: ["bippy", "solid-js", "clsx", "tailwind-merge"], + noExternal: ["bippy", "solid-js", "clsx", "tailwind-merge", "torph"], outDir: "./dist", platform: "neutral", sourcemap: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5389830a8..215a64074 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -567,6 +567,9 @@ importers: solid-js: specifier: ^1.9.10 version: 1.9.10 + torph: + specifier: ^0.0.5 + version: 0.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) devDependencies: '@babel/core': specifier: ^7.28.5 @@ -3423,8 +3426,8 @@ packages: engines: {node: '>=20'} hasBin: true - '@sourcegraph/amp@0.0.1772107648-gbe4328': - resolution: {integrity: sha512-ekkK4vs/AWgEYVRanF6bZ7T0XrlF2jTEyIzDQaJVU/mugZ6dZsQ/d+8GX6si9DuvPXnelOh6cku74WcN198zmg==} + '@sourcegraph/amp@0.0.1772323326-g24a24e': + resolution: {integrity: sha512-YVE/3NTpu+JPxeOuaLdMKHeq2G58bM1I9Tsxk7NqGkbrWASmY0Cg8mbhX/OmlSgl9H8Bwi18gHCrHYsVeBpV0Q==} engines: {node: '>=20'} hasBin: true @@ -10425,14 +10428,14 @@ snapshots: '@sourcegraph/amp-sdk@0.1.0-20251210081226-g90e3892': dependencies: - '@sourcegraph/amp': 0.0.1772107648-gbe4328 + '@sourcegraph/amp': 0.0.1772323326-g24a24e zod: 3.25.76 '@sourcegraph/amp@0.0.1767830505-ga62310': dependencies: '@napi-rs/keyring': 1.1.9 - '@sourcegraph/amp@0.0.1772107648-gbe4328': + '@sourcegraph/amp@0.0.1772323326-g24a24e': dependencies: '@napi-rs/keyring': 1.1.9 @@ -12306,7 +12309,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -12328,7 +12331,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14978,6 +14981,11 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + torph@0.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tr46@1.0.1: dependencies: punycode: 2.3.1