nudge-ui 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/bin/nudge-ui.mjs +3 -1
  2. package/dist/cli/agentCommand.d.ts +8 -0
  3. package/dist/cli/agentCommand.js +59 -0
  4. package/dist/client.mjs +878 -11
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +63 -11
  7. package/dist/hosts/astro/integration.d.ts +2 -2
  8. package/dist/hosts/astro/integration.js +4 -3
  9. package/dist/hosts/environment.d.ts +15 -0
  10. package/dist/hosts/environment.js +16 -0
  11. package/dist/hosts/next/sidecar.d.ts +4 -0
  12. package/dist/hosts/next/sidecar.js +56 -7
  13. package/dist/hosts/next/wrapper.d.ts +2 -0
  14. package/dist/hosts/next/wrapper.js +32 -3
  15. package/dist/hosts/projectBridge.d.ts +21 -0
  16. package/dist/hosts/projectBridge.js +98 -0
  17. package/dist/hosts/static/cli.d.ts +1 -1
  18. package/dist/hosts/static/cli.js +6 -1
  19. package/dist/hosts/static/manifest.d.ts +1 -1
  20. package/dist/hosts/static/manifest.js +3 -1
  21. package/dist/hosts/static/server.js +45 -3
  22. package/dist/hosts/vite/vite.d.ts +6 -1
  23. package/dist/hosts/vite/vite.js +82 -15
  24. package/dist/inspector/agent/McpConnectionDialog.d.ts +5 -4
  25. package/dist/inspector/agent/McpConnectionDialog.js +20 -37
  26. package/dist/inspector/agent/client.d.ts +16 -3
  27. package/dist/inspector/agent/client.js +114 -11
  28. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  29. package/dist/inspector/agent/connectionStatus.js +11 -4
  30. package/dist/inspector/agent/httpTransport.d.ts +8 -0
  31. package/dist/inspector/agent/httpTransport.js +78 -18
  32. package/dist/inspector/agent/protocol.d.ts +11 -3
  33. package/dist/inspector/agent/protocol.js +1 -1
  34. package/dist/inspector/agent/verification.d.ts +1 -1
  35. package/dist/inspector/agent/verification.js +69 -12
  36. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  37. package/dist/inspector/canvas/CanvasCard.js +181 -91
  38. package/dist/inspector/canvas/CanvasElementOverlay.js +288 -52
  39. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  40. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  41. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  42. package/dist/inspector/canvas/CanvasWorkspace.js +410 -72
  43. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  44. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  45. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  46. package/dist/inspector/canvas/canvasResize.js +52 -0
  47. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  48. package/dist/inspector/canvas/canvasStore.js +128 -61
  49. package/dist/inspector/canvas/frameProtocol.d.ts +82 -5
  50. package/dist/inspector/canvas/frameProtocol.js +63 -2
  51. package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -0
  52. package/dist/inspector/canvas/keyboardShortcuts.js +16 -0
  53. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  54. package/dist/inspector/canvas/linkEligibility.js +0 -29
  55. package/dist/inspector/canvas/projection.d.ts +7 -0
  56. package/dist/inspector/canvas/projection.js +118 -25
  57. package/dist/inspector/canvas/rendererBootstrap.js +42 -29
  58. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  59. package/dist/inspector/canvas/rendererElementSelector.js +385 -58
  60. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  61. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  62. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  63. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  64. package/dist/inspector/canvas/sessionStore.js +12 -4
  65. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  66. package/dist/inspector/changes/changesLog.js +31 -1
  67. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  68. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  69. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  70. package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
  71. package/dist/inspector/changes/workspaceChanges.js +29 -3
  72. package/dist/inspector/clientManifest.d.ts +7 -0
  73. package/dist/inspector/clientManifest.js +12 -0
  74. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  75. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  76. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  77. package/dist/inspector/index.d.ts +2 -0
  78. package/dist/inspector/index.js +80 -32
  79. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  80. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  81. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  82. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  83. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  84. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  85. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  86. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  87. package/dist/inspector/inline-text/textEditChange.js +165 -0
  88. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  89. package/dist/inspector/inspection/selectionProperty.js +1 -0
  90. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  91. package/dist/inspector/overlay/DomNavigator.js +40 -0
  92. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  93. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  94. package/dist/inspector/overlay/clickPolicy.js +34 -3
  95. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  96. package/dist/inspector/overlay/elementSelector.js +18 -7
  97. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  98. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  99. package/dist/inspector/overlay/spacingGestures.js +351 -0
  100. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  101. package/dist/inspector/overlay/structuralGestures.js +12 -10
  102. package/dist/inspector/projection/managedStylesheet.js +4 -1
  103. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  104. package/dist/inspector/projection/renderedInstance.js +5 -1
  105. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  106. package/dist/inspector/projection/structuralProjection.js +85 -1
  107. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  108. package/dist/inspector/projection/textProjection.d.ts +2 -0
  109. package/dist/inspector/projection/textProjection.js +10 -4
  110. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  111. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  112. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  113. package/dist/inspector/prompt/generatePrompt.js +19 -2
  114. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  115. package/dist/inspector/runtime/editorShell.js +4 -0
  116. package/dist/inspector/runtime/runtimeConfig.d.ts +5 -1
  117. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  118. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  119. package/dist/inspector/selection/hierarchy.js +8 -0
  120. package/dist/inspector/selection/selectionStore.js +2 -0
  121. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  122. package/dist/inspector/settings/SettingsDialog.js +12 -2
  123. package/dist/inspector/shell/AppShell.js +5 -1
  124. package/dist/inspector/shell/ChangesLog.js +26 -22
  125. package/dist/inspector/shell/CopyPromptButton.js +102 -19
  126. package/dist/inspector/shell/EmptyState.js +1 -1
  127. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  128. package/dist/inspector/shell/InspectorShell.js +38 -42
  129. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  130. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  131. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  132. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  133. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  134. package/dist/inspector/shell/originalPreview.js +61 -0
  135. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  136. package/dist/inspector/shell/shortcuts.js +15 -0
  137. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  138. package/dist/inspector/sketch/SketchChanges.js +72 -0
  139. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  140. package/dist/inspector/sketch/SketchEditor.js +402 -0
  141. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  142. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  143. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  144. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  145. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  146. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  147. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  148. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  149. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  150. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  151. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  152. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  153. package/dist/inspector/sketch/capture.d.ts +55 -0
  154. package/dist/inspector/sketch/capture.js +350 -0
  155. package/dist/inspector/sketch/freehand.d.ts +13 -0
  156. package/dist/inspector/sketch/freehand.js +64 -0
  157. package/dist/inspector/sketch/geometry.d.ts +17 -0
  158. package/dist/inspector/sketch/geometry.js +98 -0
  159. package/dist/inspector/sketch/handoff.d.ts +21 -0
  160. package/dist/inspector/sketch/handoff.js +72 -0
  161. package/dist/inspector/sketch/interaction.d.ts +21 -0
  162. package/dist/inspector/sketch/interaction.js +98 -0
  163. package/dist/inspector/sketch/model.d.ts +104 -0
  164. package/dist/inspector/sketch/model.js +282 -0
  165. package/dist/inspector/sketch/persistence.d.ts +21 -0
  166. package/dist/inspector/sketch/persistence.js +205 -0
  167. package/dist/inspector/sketch/raster.d.ts +22 -0
  168. package/dist/inspector/sketch/raster.js +203 -0
  169. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  170. package/dist/inspector/sketch/sketchNote.js +30 -0
  171. package/dist/inspector/sketch/store.d.ts +47 -0
  172. package/dist/inspector/sketch/store.js +434 -0
  173. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  174. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  175. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  176. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  177. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  178. package/dist/inspector/styleEditors/GapField.js +2 -2
  179. package/dist/inspector/styleEditors/GridSection.js +1 -1
  180. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  181. package/dist/inspector/styleEditors/SpacingBox.js +31 -33
  182. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  183. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  184. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  185. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  186. package/dist/inspector/styleEditors/nudgeValue.js +93 -5
  187. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  188. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  189. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  190. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  191. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  192. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  193. package/dist/inspector/tokens/TokenField.js +130 -7
  194. package/dist/inspector/tokens/TokensPanel.js +2 -1
  195. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  196. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  197. package/dist/inspector/tokens/editActions.d.ts +2 -0
  198. package/dist/inspector/tokens/editActions.js +2 -2
  199. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  200. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  201. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  202. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  203. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  204. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  205. package/dist/inspector/tokens/resolution.d.ts +3 -26
  206. package/dist/inspector/tokens/resolution.js +44 -735
  207. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  208. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  209. package/dist/inspector/ui/Button.d.ts +1 -1
  210. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  211. package/dist/inspector/ui/Disclosure.js +13 -0
  212. package/dist/inspector/ui/SideValuesField.js +2 -2
  213. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  214. package/dist/inspector/ui/StatusCallout.js +2 -2
  215. package/dist/inspector/ui/dragCursor.d.ts +8 -0
  216. package/dist/inspector/ui/dragCursor.js +21 -0
  217. package/dist/inspector/ui/styles.js +21 -17
  218. package/dist/internal/agent-protocol.d.ts +65 -1
  219. package/dist/internal/agent-protocol.js +183 -3
  220. package/dist/nudge-ui.mjs +276 -58
  221. package/dist/transport/editor.d.ts +47 -0
  222. package/dist/transport/editor.js +163 -0
  223. package/dist/transport/index.d.ts +1 -0
  224. package/dist/transport/index.js +1 -0
  225. package/dist/transport/routes.d.ts +10 -0
  226. package/dist/transport/routes.js +10 -0
  227. package/package.json +3 -1
  228. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  229. package/dist/inspector/shell/DomNavigation.js +0 -30
  230. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  231. package/dist/inspector/shell/panelLayout.js +0 -29
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from "react";
3
- import { IconArrowUpRight, IconColorSwatch, IconLayoutSidebarRight, IconSettings } from "@tabler/icons-react";
3
+ import { IconLayoutSidebarRight, IconSettings } from "@tabler/icons-react";
4
4
  import { useInspectorOpen, toggleInspector, setInspectorOpen } from "./openStore.js";
5
- import { useSelectedElement, useSelectedElements, useHierarchy, setSelectedElement, removeSelectedElement, } from "../selection/selectionStore.js";
6
- import { InspectorOverlay } from "../overlay/InspectorOverlay.js";
5
+ import { useSelectedElement, useSelectedElements, setSelectedElement, removeSelectedElement, } from "../selection/selectionStore.js";
7
6
  import { findTokenRow } from "../styleEditors/rowLookup.js";
8
7
  import { useBrowserCssInspection } from "../inspection/useBrowserCssInspection.js";
9
8
  import { resolveSelectionFromElement } from "../selection/resolveSelection.js";
10
9
  import { SpacingBox } from "../styleEditors/SpacingBox.js";
11
10
  import { Typography } from "../styleEditors/Typography.js";
11
+ import { isTextRelevant } from "../styleEditors/textRelevance.js";
12
12
  import { ColorPicker } from "../styleEditors/ColorPicker.js";
13
13
  import { BorderEditor } from "../styleEditors/BorderEditor.js";
14
14
  import { AppearanceSection } from "../styleEditors/AppearanceSection.js";
@@ -20,27 +20,36 @@ import { countSourceSiteMatches, getEditScope, relinkElement, sourceSiteSelector
20
20
  import { canEditStyles } from "../tokens/editActions.js";
21
21
  import { Button } from "../ui/Button.js";
22
22
  import { CopyPromptButton } from "./CopyPromptButton.js";
23
+ import { PeekOriginalButton } from "./PeekOriginalButton.js";
23
24
  import { StatusCallout } from "../ui/StatusCallout.js";
24
25
  import { IconButton } from "../ui/IconButton.js";
25
26
  import { UI_STYLES } from "../ui/styles.js";
26
27
  import { getActiveStyleState, setActiveStyleState } from "./styleState.js";
27
28
  import { isEditableEvent } from "./shortcuts.js";
28
- import { clearInspectorLayout, setInspectorLayoutOpen } from "./panelLayout.js";
29
29
  import { formatInspectorLabel } from "../ui/labels.js";
30
30
  import { FieldRow } from "../ui/FieldRow.js";
31
31
  import { Select } from "../ui/Select.js";
32
- import { enterCanvas, useCanvasMode } from "../canvas/canvasStore.js";
33
32
  import { clearRestoreCount, clearSession } from "../canvas/sessionStore.js";
33
+ import { hasWriteLease } from "../canvas/workspaceLease.js";
34
+ import { useFocusedCardId, useSelectedCardId, } from "../canvas/canvasStore.js";
35
+ import { getActiveCanvasFrame } from "../canvas/activeCanvasDocument.js";
34
36
  import { getElementWindow } from "../runtime/domRealm.js";
35
37
  import { deleteElement, nudgeElement } from "../overlay/structuralGestures.js";
36
38
  import { AtRuleContextProvider } from "../ui/AtRuleContext.js";
37
39
  import { ComponentPropsSection } from "../componentSemantics/ComponentPropsSection.js";
38
- import { cancelInlineTextEdit, disposeInlineTextEdit, isInlineTextEditingActive, useInlineTextSession } from "../inline-text/inlineTextEditor.js";
40
+ import { cancelInlineTextEdit, isInlineTextEditingActive, useInlineTextDiagnostic, } from "../inline-text/inlineTextEditor.js";
41
+ import { getInlineTextFeedback } from "../inline-text/inlineTextFeedback.js";
39
42
  import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
40
- import { DomNavigation } from "./DomNavigation.js";
41
43
  import { EmptyState } from "./EmptyState.js";
42
44
  import { createStyleSelection } from "../selection/styleSelection.js";
43
45
  import { intersectTokenEntries } from "../inspection/selectionProperty.js";
46
+ import { isNudgeUiDev } from "../runtime/devFlag.js";
47
+ import { isDemoRuntime } from "../runtime/runtimeConfig.js";
48
+ import { SketchWorkspace } from "../sketch/SketchWorkspace.js";
49
+ import { cancelSketchInteraction, useSketchInteractionActive } from "../sketch/interaction.js";
50
+ import { clearSketchClipboardHandoff } from "../sketch/handoff.js";
51
+ import { clearSketchesForProject } from "../sketch/store.js";
52
+ import { closeSketchNote } from "../sketch/sketchNote.js";
44
53
  function findFirstTokenRow(rows, properties) {
45
54
  for (const property of properties) {
46
55
  const row = findTokenRow(rows, property);
@@ -55,13 +64,6 @@ function findFirstTokenRow(rows, properties) {
55
64
  return null;
56
65
  }
57
66
  export { toggleInspector, setInspectorOpen };
58
- let inspectorHost = null;
59
- export function setInspectorHost(host) {
60
- inspectorHost = host;
61
- }
62
- function resolveHost() {
63
- return inspectorHost ?? document.getElementById("nudge-ui-root") ?? document.body;
64
- }
65
67
  function nodeMatchesSelector(node, selector) {
66
68
  if (!selector || node.nodeType !== node.ELEMENT_NODE)
67
69
  return false;
@@ -87,24 +89,20 @@ function scopeMutationAffectsSelection(records, selected) {
87
89
  export function InspectorShell() {
88
90
  const isOpen = useInspectorOpen();
89
91
  const runtimeConfig = useNudgeUiRuntimeConfig();
90
- const canvasEnabled = runtimeConfig.capabilities.canvas;
91
- const domNavigationEnabled = runtimeConfig.capabilities.domNavigation === true;
92
- const activeCanvasMode = useCanvasMode();
93
- const canvasMode = canvasEnabled ? activeCanvasMode : "inspect";
92
+ const sketchEnabled = isNudgeUiDev() && !isDemoRuntime();
93
+ const sketchActive = useSketchInteractionActive();
94
+ const selectedCardId = useSelectedCardId();
95
+ const focusedCardId = useFocusedCardId();
96
+ const sketchHost = selectedCardId || focusedCardId ? getActiveCanvasFrame() : null;
94
97
  const selected = useSelectedElement();
95
98
  const selectedElements = useSelectedElements();
96
- const hierarchy = useHierarchy();
97
- const inlineTextSession = useInlineTextSession();
99
+ const inlineTextFeedback = getInlineTextFeedback(useInlineTextDiagnostic());
98
100
  const [settingsOpen, setSettingsOpen] = useState(false);
99
101
  const [settingsSection, setSettingsSection] = useState("instructions");
100
102
  const [scopeRevision, refreshScope] = useState(0);
101
103
  const [styleState, setStyleState] = useState(getActiveStyleState());
102
104
  const cssInspection = useBrowserCssInspection(selectedElements, styleState);
103
105
  const isMultiSelection = selectedElements.length > 1;
104
- useEffect(() => {
105
- setInspectorLayoutOpen(isOpen);
106
- return clearInspectorLayout;
107
- }, [isOpen]);
108
106
  useEffect(() => {
109
107
  // A new selection should never inherit an incidental state from the
110
108
  // previous element. Base is the inspector's deliberate default.
@@ -147,7 +145,7 @@ export function InspectorShell() {
147
145
  return () => observers.forEach((observer) => observer.disconnect());
148
146
  }, [selectedElements, scopeRevision]);
149
147
  useEffect(() => {
150
- if (!isOpen)
148
+ if (sketchActive)
151
149
  return;
152
150
  function onKeydown(event) {
153
151
  if (isInlineTextEditingActive())
@@ -200,13 +198,14 @@ export function InspectorShell() {
200
198
  }
201
199
  window.addEventListener("keydown", onKeydown);
202
200
  return () => window.removeEventListener("keydown", onKeydown);
203
- }, [isOpen, isMultiSelection, selected]);
201
+ }, [isMultiSelection, selected, sketchActive]);
204
202
  const inspectionSnapshot = cssInspection.element;
205
203
  const styleSelection = useMemo(() => createStyleSelection(selectedElements, cssInspection.elements, selected), [cssInspection.elements, selected, selectedElements]);
206
204
  const selectionEditable = useMemo(() => styleSelection ? canEditStyles(styleSelection.target) : false, [scopeRevision, styleSelection]);
207
205
  const tokenEntries = useMemo(() => intersectTokenEntries(cssInspection.elements.map((snapshot) => snapshot.availableTokens)), [cssInspection.elements]);
208
206
  const tokenRows = useMemo(() => styleSelection ? [...styleSelection.primaryRows] : [], [styleSelection]);
209
207
  const availableInteractionStates = inspectionSnapshot?.availableStates ?? [];
208
+ const showText = selectedElements.length > 0 && selectedElements.every((element, index) => isTextRelevant(element.domElement, cssInspection.elements[index]?.properties ?? []));
210
209
  const showInteractionState = !isMultiSelection && availableInteractionStates.length > 2;
211
210
  const paintedBackgroundRow = findFirstTokenRow(tokenRows, ["background-color", "background"]);
212
211
  const backgroundTokenRow = useMemo(() => {
@@ -227,24 +226,24 @@ export function InspectorShell() {
227
226
  function refreshScopeState() {
228
227
  refreshScope((revision) => revision + 1);
229
228
  }
230
- function handleCanvasModeButton() {
231
- // Inline text editing is controller-owned. Canvas renderer documents
232
- // receive projections only, so dispose the active controller session
233
- // before mounting cards rather than probing a stale iframe document.
234
- disposeInlineTextEdit("frame-disposed");
235
- enterCanvas();
236
- }
237
229
  function openSettings(section) {
238
230
  setSettingsSection(section);
239
231
  setSettingsOpen(true);
240
232
  }
241
- return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "inspector-styles", children: UI_STYLES }), canvasMode === "inspect" && _jsx(InspectorOverlay, { host: resolveHost() }), _jsxs("div", { className: "panel", "data-open": isOpen ? "true" : "false", children: [_jsxs("div", { className: "panel__tabs", "aria-label": "Inspector controls", children: [_jsxs("div", { className: "panel__header-row", "data-test": "inspect-tab", children: [_jsx(IconButton, { variant: "quiet", label: "Collapse inspector", "data-test": "collapse-inspector", style: { marginLeft: "-8px" }, onClick: () => {
233
+ function clearInspectorSession() {
234
+ clearSession();
235
+ clearRestoreCount();
236
+ if (!hasWriteLease())
237
+ return;
238
+ cancelSketchInteraction();
239
+ closeSketchNote();
240
+ clearSketchClipboardHandoff();
241
+ void clearSketchesForProject(runtimeConfig.projectId).catch(() => undefined);
242
+ }
243
+ return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "inspector-styles", children: UI_STYLES }), _jsxs("div", { className: "panel", "data-open": isOpen ? "true" : "false", children: [_jsxs("div", { className: "panel__tabs", "aria-label": "Inspector controls", children: [_jsxs("div", { className: "panel__header-row", "data-test": "inspect-tab", children: [_jsx(IconButton, { variant: "quiet", label: "Collapse inspector", "data-test": "collapse-inspector", disabled: sketchActive, style: { marginLeft: "-8px" }, onClick: () => {
242
244
  cancelInlineTextEdit();
243
245
  setInspectorOpen(false);
244
- }, children: _jsx(IconLayoutSidebarRight, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "panel__header-actions", children: [_jsx(IconButton, { variant: "quiet", "data-test": "tokens-button", label: "Tokens", title: "Tokens", onClick: () => openSettings("tokens"), children: _jsx(IconColorSwatch, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsx(IconButton, { variant: "quiet", label: "Settings", title: "Settings", "data-test": "settings-button", onClick: () => openSettings("instructions"), children: _jsx(IconSettings, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), canvasEnabled && canvasMode !== "canvas" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "panel__header-divider", "aria-hidden": "true" }), _jsxs(Button, { variant: "quiet", className: "panel__canvas-button", "data-test": "mode-canvas", type: "button", onClick: handleCanvasModeButton, children: ["Canvas", _jsx(IconArrowUpRight, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" })] })] })) : null] })] }), _jsx("div", { className: "panel__copy-row", children: _jsx(CopyPromptButton, { settingsOpen: settingsOpen, settingsSection: settingsSection, onOpenSettings: openSettings, onSettingsOpenChange: setSettingsOpen }) })] }), _jsxs("div", { className: "panel__body", children: [inlineTextSession && (inlineTextSession.bindingChoices.length > 1 ||
245
- inlineTextSession.scopeChoices.length > 0) ? (_jsxs("section", { className: "inline-text-editor", "data-test": "inline-text-editor", children: [_jsx("div", { className: "inline-text-editor__binding", "data-test": "inline-text-binding", children: inlineTextSession.binding.kind === "component-prop"
246
- ? `${inlineTextSession.binding.target.componentName}.${inlineTextSession.binding.property}`
247
- : "Rendered text" }), inlineTextSession.bindingChoices.length > 1 ? (_jsxs("div", { className: "inline-text-editor__chooser", "data-test": "inline-binding-chooser", children: [_jsx("div", { className: "inline-text-editor__chooser-label", children: "Choose binding" }), _jsx("div", { className: "inline-text-editor__chooser-options", role: "group", "aria-label": "Text binding", children: inlineTextSession.bindingChoices.map((choice, index) => (_jsxs(Button, { size: "compact", variant: inlineTextSession.selectedBindingIndex === index ? "primary" : "quiet", "data-test": "inline-binding-choice", "data-index": index, "aria-pressed": inlineTextSession.selectedBindingIndex === index, onMouseDown: (event) => event.preventDefault(), onClick: () => inlineTextSession.chooseBinding(index), children: [choice.binding.target.componentName, ".", choice.binding.property] }, `${choice.binding.target.callsiteId}:${choice.binding.property}`))) })] })) : null, inlineTextSession.scopeChoices.length > 0 ? (_jsxs("div", { className: "inline-text-editor__chooser", "data-test": "inline-scope-chooser", children: [_jsx("div", { className: "inline-text-editor__chooser-label", children: "Apply to" }), _jsx("div", { className: "inline-text-editor__chooser-options", role: "group", "aria-label": "Text edit scope", children: inlineTextSession.scopeChoices.map((scope) => (_jsx(Button, { size: "compact", variant: inlineTextSession.scope === scope ? "primary" : "quiet", "data-test": `inline-scope-${scope}`, "aria-pressed": inlineTextSession.scope === scope, onMouseDown: (event) => event.preventDefault(), onClick: () => inlineTextSession.chooseScope(scope), children: scope === "source-site" ? "All outputs" : "This rendered item" }, scope))) })] })) : null, _jsxs("div", { className: "inline-text-editor__actions", children: [_jsx(Button, { size: "compact", variant: "secondary", "data-test": "inline-text-cancel", onClick: () => inlineTextSession.cancel(), children: "Cancel" }), _jsx(Button, { size: "compact", "data-test": "inline-text-commit", disabled: inlineTextSession.bindingChoices.length > 1 && inlineTextSession.selectedBindingIndex === null, onClick: () => inlineTextSession.commit(), children: "Done" })] })] })) : null, selected ? (_jsxs(_Fragment, { children: [domNavigationEnabled && !isMultiSelection ? _jsx(DomNavigation, { selected: selected, hierarchy: hierarchy }) : null, isMultiSelection || showInteractionState || hasEditScopeCallout ? (_jsxs("div", { className: `selection${hasEditScopeCallout ? "" : " selection--without-scope-callout"}`, "data-test": "selection", "data-selected-cid": selected.cid, "data-selected-src": selected.src, "data-selected-count": selectedElements.length, children: [isMultiSelection ? (_jsx("div", { className: "selection__summary", "data-test": "multi-selection-summary", children: _jsxs("span", { className: "selection__count", children: [selectedElements.length, " elements selected"] }) })) : null, hasEditScopeCallout ? (_jsx(StatusCallout, { tone: editScope === "rendered-instance" ? "neutral" : "accent", "data-test": "edit-scope", "data-lost": "false", children: editScope === "rendered-instance" ? (_jsxs("div", { className: "scope__unlinked", children: [_jsx("span", { children: "Element unlinked" }), _jsx(Button, { size: "compact", className: "scope__action", "data-test": "relink-element", onClick: () => {
246
+ }, children: _jsx(IconLayoutSidebarRight, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "panel__header-actions", children: [_jsx(PeekOriginalButton, {}), _jsx(IconButton, { variant: "quiet", label: "Settings", title: "Settings", "data-test": "settings-button", onClick: () => openSettings("instructions"), children: _jsx(IconSettings, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "panel__copy-row", children: _jsx(CopyPromptButton, { settingsOpen: settingsOpen, settingsSection: settingsSection, onOpenSettings: openSettings, onSettingsOpenChange: setSettingsOpen }) })] }), _jsxs("div", { className: "panel__body", children: [inlineTextFeedback ? (_jsx(StatusCallout, { tone: "danger", className: "inline-text-diagnostic", "data-test": "inline-text-diagnostic", role: "status", "aria-live": "polite", "aria-atomic": "true", children: _jsx("span", { children: inlineTextFeedback.message }) })) : null, selected ? (_jsxs(_Fragment, { children: [isMultiSelection || showInteractionState || hasEditScopeCallout ? (_jsxs("div", { className: `selection${hasEditScopeCallout ? "" : " selection--without-scope-callout"}`, "data-test": "selection", "data-selected-cid": selected.cid, "data-selected-src": selected.src, "data-selected-count": selectedElements.length, children: [isMultiSelection ? (_jsx("div", { className: "selection__summary", "data-test": "multi-selection-summary", children: _jsxs("span", { className: "selection__count", children: [selectedElements.length, " elements selected"] }) })) : null, hasEditScopeCallout ? (_jsx(StatusCallout, { tone: editScope === "rendered-instance" ? "neutral" : "accent", "data-test": "edit-scope", "data-lost": "false", children: editScope === "rendered-instance" ? (_jsxs("div", { className: "scope__unlinked", children: [_jsx("span", { children: "Element unlinked" }), _jsx(Button, { size: "compact", className: "scope__action", "data-test": "relink-element", onClick: () => {
248
247
  const overrideId = relinkElement(selected.domElement);
249
248
  if (overrideId)
250
249
  discardChangesForInstanceOverride(overrideId);
@@ -261,8 +260,5 @@ export function InspectorShell() {
261
260
  return;
262
261
  setActiveStyleState(nextState);
263
262
  setStyleState(nextState);
264
- } }) })) : null] })) : null, !isMultiSelection ? _jsx(ComponentPropsSection, { selected: selected }) : null, selectionEditable ? (_jsx(AtRuleContextProvider, { rows: tokenRows, children: _jsxs("div", { className: "style-editors", "data-test": "style-editors", children: [_jsx(LayoutSection, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `layout-${styleState}`), _jsx(SpacingBox, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `spacing-${styleState}`), _jsx(AppearanceSection, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `appearance-${styleState}`), _jsx(Typography, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `type-${styleState}`), _jsx(ColorPicker, { element: selected, selection: styleSelection, property: "color", entries: tokenEntries, tokenRow: findTokenRow(tokenRows, "color") }, `color-${styleState}`), _jsx(ColorPicker, { element: selected, selection: styleSelection, property: "background-color", entries: tokenEntries, tokenRow: backgroundTokenRow }, `background-${styleState}`), _jsx(BorderEditor, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `border-${styleState}`), _jsx(BoxShadowEditor, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `box-shadow-${styleState}`)] }) })) : (_jsx(StatusCallout, { tone: "neutral", "data-test": "multi-selection-uneditable", children: "These repeated items are indistinguishable, so Nudge UI cannot safely target only part of the group. Select every repeated item to edit them together." }))] })) : (_jsx(EmptyState, {})), _jsx(ChangesLog, { onClearSession: () => {
265
- clearSession();
266
- clearRestoreCount();
267
- } })] })] }), !isOpen ? (_jsx("div", { className: "panel__restore", children: _jsx(IconButton, { variant: "quiet", label: "Show inspector", className: "panel__restore-button", "data-test": "show-inspector", onClick: () => setInspectorOpen(true), children: _jsx(IconLayoutSidebarRight, { size: 16, stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }) }) })) : null] }));
263
+ } }) })) : null] })) : null, !isMultiSelection ? _jsx(ComponentPropsSection, { selected: selected }) : null, selectionEditable ? (_jsx(AtRuleContextProvider, { rows: tokenRows, children: _jsxs("div", { className: "style-editors", "data-test": "style-editors", children: [_jsx(LayoutSection, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `layout-${styleState}`), _jsx(SpacingBox, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `spacing-${styleState}`), _jsx(AppearanceSection, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `appearance-${styleState}`), showText ? _jsxs(_Fragment, { children: [_jsx(Typography, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `type-${styleState}`), _jsx(ColorPicker, { element: selected, selection: styleSelection, property: "color", entries: tokenEntries, tokenRow: findTokenRow(tokenRows, "color") }, `color-${styleState}`)] }) : null, _jsx(ColorPicker, { element: selected, selection: styleSelection, property: "background-color", entries: tokenEntries, tokenRow: backgroundTokenRow }, `background-${styleState}`), _jsx(BorderEditor, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `border-${styleState}`), _jsx(BoxShadowEditor, { element: selected, selection: styleSelection, entries: tokenEntries, tokenRows: tokenRows }, `box-shadow-${styleState}`)] }) })) : (_jsx(StatusCallout, { tone: "neutral", "data-test": "multi-selection-uneditable", children: "These repeated items are indistinguishable, so Nudge UI cannot safely target only part of the group. Select every repeated item to edit them together." }))] })) : (_jsx(EmptyState, {})), _jsx(ChangesLog, { onClearSession: clearInspectorSession })] })] }), sketchEnabled ? (_jsx(SketchWorkspace, { hostElement: sketchHost, projectId: runtimeConfig.projectId })) : null, !isOpen ? (_jsx("div", { className: "panel__restore", children: _jsx(IconButton, { variant: "quiet", label: "Show inspector", className: "panel__restore-button", "data-test": "show-inspector", onClick: () => setInspectorOpen(true), children: _jsx(IconLayoutSidebarRight, { size: 16, stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }) }) })) : null] }));
268
264
  }
@@ -0,0 +1,7 @@
1
+ import { type ReactElement } from "react";
2
+ /**
3
+ * Press-and-hold button that previews the page without inspector changes
4
+ * (NUD-30). Releasing restores the edited preview; canonical intent is never
5
+ * cleared, so nothing is lost while held.
6
+ */
7
+ export declare function PeekOriginalButton(): ReactElement;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useSyncExternalStore } from "react";
3
+ import { IconButton } from "../ui/IconButton.js";
4
+ import { PeekOriginalIcon } from "./PeekOriginalIcon.js";
5
+ import { workspaceChangeStore } from "../changes/workspaceChanges.js";
6
+ import { setOriginalPreviewActive, useOriginalPreviewActive, } from "./originalPreview.js";
7
+ function useWorkspaceChangeCount() {
8
+ return useSyncExternalStore(workspaceChangeStore.subscribe, () => {
9
+ const snapshot = workspaceChangeStore.getSnapshot();
10
+ return snapshot.changes.length + snapshot.structuralChanges.length;
11
+ }, () => 0);
12
+ }
13
+ /**
14
+ * Press-and-hold button that previews the page without inspector changes
15
+ * (NUD-30). Releasing restores the edited preview; canonical intent is never
16
+ * cleared, so nothing is lost while held.
17
+ */
18
+ export function PeekOriginalButton() {
19
+ const active = useOriginalPreviewActive();
20
+ const changeCount = useWorkspaceChangeCount();
21
+ const disabled = changeCount === 0;
22
+ useEffect(() => {
23
+ if (!active)
24
+ return;
25
+ const release = () => setOriginalPreviewActive(false);
26
+ window.addEventListener("pointerup", release);
27
+ window.addEventListener("pointercancel", release);
28
+ window.addEventListener("blur", release);
29
+ return () => {
30
+ window.removeEventListener("pointerup", release);
31
+ window.removeEventListener("pointercancel", release);
32
+ window.removeEventListener("blur", release);
33
+ };
34
+ }, [active]);
35
+ useEffect(() => () => {
36
+ setOriginalPreviewActive(false);
37
+ }, []);
38
+ return (_jsx(IconButton, { variant: "quiet", "data-test": "peek-original-button", label: "Hold to view original", title: disabled ? "No changes to preview" : "Hold to view original", "aria-pressed": active, "data-active": active ? "true" : "false", disabled: disabled, onPointerDown: (event) => {
39
+ if (disabled)
40
+ return;
41
+ if (event.pointerType === "mouse" && event.button !== 0)
42
+ return;
43
+ event.currentTarget.setPointerCapture?.(event.pointerId);
44
+ setOriginalPreviewActive(true);
45
+ }, onPointerUp: () => setOriginalPreviewActive(false), onPointerCancel: () => setOriginalPreviewActive(false), onLostPointerCapture: () => setOriginalPreviewActive(false), onKeyDown: (event) => {
46
+ if (disabled || event.repeat)
47
+ return;
48
+ if (event.key === " " || event.key === "Enter")
49
+ setOriginalPreviewActive(true);
50
+ }, onKeyUp: (event) => {
51
+ if (event.key === " " || event.key === "Enter")
52
+ setOriginalPreviewActive(false);
53
+ }, onBlur: () => setOriginalPreviewActive(false), onContextMenu: (event) => {
54
+ if (active)
55
+ event.preventDefault();
56
+ }, children: _jsx(PeekOriginalIcon, {}) }));
57
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactElement } from "react";
2
+ /**
3
+ * Before/after comparison glyph for the hold-to-view-original button.
4
+ * Custom artwork rendered with the shared inspector icon tokens so it
5
+ * matches the neighboring header icons.
6
+ */
7
+ export declare function PeekOriginalIcon(): ReactElement;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Before/after comparison glyph for the hold-to-view-original button.
4
+ * Custom artwork rendered with the shared inspector icon tokens so it
5
+ * matches the neighboring header icons.
6
+ */
7
+ export function PeekOriginalIcon() {
8
+ return (_jsxs("svg", { viewBox: "0 0 24 24", width: "var(--icon-size-small)", height: "var(--icon-size-small)", "aria-hidden": "true", children: [_jsx("path", { d: "M8.66667 5L6 5C4.34315 5 3 6.34315 3 8L3 16C3 17.6569 4.34315 19 6 19L12 19", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeWidth: "var(--icon-stroke-width)" }), _jsx("path", { d: "M13 5L18 5C19.6569 5 21 6.34315 21 8L21 16C21 17.6569 19.6569 19 18 19L16.3333 19", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeWidth: "var(--icon-stroke-width)" }), _jsx("line", { x1: "13.7721", y1: "22.2984", x2: "8.29835", y2: "2.22788", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeWidth: "var(--icon-stroke-width)" })] }));
9
+ }
@@ -0,0 +1,9 @@
1
+ export declare function isOriginalPreviewActive(): boolean;
2
+ export declare function subscribeOriginalPreview(listener: () => void): () => void;
3
+ export declare function useOriginalPreviewActive(): boolean;
4
+ /**
5
+ * Hold-to-view-original (NUD-30). While active the host document shows the
6
+ * page without any inspector changes. Canonical intent stays in the workspace
7
+ * store, so releasing restores the edited preview without loss.
8
+ */
9
+ export declare function setOriginalPreviewActive(next: boolean): void;
@@ -0,0 +1,61 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { workspaceChangeStore } from "../changes/workspaceChanges.js";
3
+ import { applyHostWorkspaceProjection, compileWorkspaceProjection, } from "../projection/workspaceProjection.js";
4
+ import { isEditorShellDocument } from "../runtime/editorShell.js";
5
+ let previewingOriginal = false;
6
+ const listeners = new Set();
7
+ export function isOriginalPreviewActive() {
8
+ return previewingOriginal;
9
+ }
10
+ export function subscribeOriginalPreview(listener) {
11
+ listeners.add(listener);
12
+ return () => {
13
+ listeners.delete(listener);
14
+ };
15
+ }
16
+ export function useOriginalPreviewActive() {
17
+ return useSyncExternalStore(subscribeOriginalPreview, () => previewingOriginal, () => false);
18
+ }
19
+ function notifyOriginalPreviewListeners() {
20
+ for (const listener of listeners) {
21
+ try {
22
+ listener();
23
+ }
24
+ catch {
25
+ /* Subscriber errors must not break the preview toggle. */
26
+ }
27
+ }
28
+ }
29
+ function applyEmptyHostProjection() {
30
+ if (isEditorShellDocument())
31
+ return;
32
+ const { revision } = workspaceChangeStore.getSnapshot();
33
+ applyHostWorkspaceProjection({
34
+ sourceRevision: revision,
35
+ managedStyles: { rules: [], css: "" },
36
+ instanceOverrides: [],
37
+ structuralChanges: [],
38
+ textContentChanges: [],
39
+ componentOverrides: [],
40
+ });
41
+ }
42
+ function applyCanonicalHostProjection() {
43
+ if (isEditorShellDocument())
44
+ return;
45
+ applyHostWorkspaceProjection(compileWorkspaceProjection(workspaceChangeStore.getSnapshot()));
46
+ }
47
+ /**
48
+ * Hold-to-view-original (NUD-30). While active the host document shows the
49
+ * page without any inspector changes. Canonical intent stays in the workspace
50
+ * store, so releasing restores the edited preview without loss.
51
+ */
52
+ export function setOriginalPreviewActive(next) {
53
+ if (next === previewingOriginal)
54
+ return;
55
+ previewingOriginal = next;
56
+ if (previewingOriginal)
57
+ applyEmptyHostProjection();
58
+ else
59
+ applyCanonicalHostProjection();
60
+ notifyOriginalPreviewListeners();
61
+ }
@@ -1,3 +1,6 @@
1
1
  export declare function isEditableTarget(target: EventTarget | null): boolean;
2
2
  export declare function isEditableEvent(event: KeyboardEvent): boolean;
3
+ export declare const SEND_PROMPT_HOTKEY_EVENT = "nudge-ui-send-prompt-hotkey";
4
+ export declare function isSendPromptShortcut(event: KeyboardEvent): boolean;
3
5
  export declare function isInspectorToggleShortcut(event: KeyboardEvent): boolean;
6
+ export declare function isOriginalPreviewShortcut(event: KeyboardEvent): boolean;
@@ -14,6 +14,12 @@ function deeplyFocusedElement() {
14
14
  export function isEditableEvent(event) {
15
15
  return event.composedPath().some(isEditableTarget) || isEditableTarget(deeplyFocusedElement());
16
16
  }
17
+ export const SEND_PROMPT_HOTKEY_EVENT = "nudge-ui-send-prompt-hotkey";
18
+ export function isSendPromptShortcut(event) {
19
+ if (event.repeat || event.altKey || isEditableEvent(event))
20
+ return false;
21
+ return event.shiftKey && !event.metaKey && !event.ctrlKey && event.code === "KeyS";
22
+ }
17
23
  export function isInspectorToggleShortcut(event) {
18
24
  if (isEditableEvent(event))
19
25
  return false;
@@ -23,3 +29,12 @@ export function isInspectorToggleShortcut(event) {
23
29
  return true;
24
30
  return event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.code === "KeyI";
25
31
  }
32
+ export function isOriginalPreviewShortcut(event) {
33
+ if (event.repeat)
34
+ return false;
35
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
36
+ return false;
37
+ if (event.code !== "Backslash")
38
+ return false;
39
+ return !isEditableEvent(event);
40
+ }
@@ -0,0 +1,2 @@
1
+ import type { ReactElement } from "react";
2
+ export declare function SketchChanges(): ReactElement | null;
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { IconCheck, IconCopy, IconDownload, IconEdit, IconRefresh, IconTrash } from "@tabler/icons-react";
4
+ import { Button } from "../ui/Button.js";
5
+ import { StatusCallout } from "../ui/StatusCallout.js";
6
+ import { copyImageToClipboard, downloadImage } from "./raster.js";
7
+ import { editSketch } from "./SketchWorkspace.js";
8
+ import { removeSketch, requeueSketch, useSketchStore } from "./store.js";
9
+ import {} from "./model.js";
10
+ function routeLabel(url) {
11
+ try {
12
+ const parsed = new URL(url);
13
+ return `${parsed.pathname}${parsed.search}` || "/";
14
+ }
15
+ catch {
16
+ return url;
17
+ }
18
+ }
19
+ function statusLabel(status) {
20
+ switch (status) {
21
+ case "dispatching": return "Sending";
22
+ case "handing-off": return "Handing off";
23
+ case "needs-review": return "Needs review";
24
+ case "unknown": return "Unknown";
25
+ default: return "Pending";
26
+ }
27
+ }
28
+ function Thumbnail({ item }) {
29
+ const [url, setUrl] = useState(null);
30
+ useEffect(() => {
31
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function")
32
+ return;
33
+ const next = URL.createObjectURL(item.document.annotatedImage);
34
+ setUrl(next);
35
+ return () => URL.revokeObjectURL(next);
36
+ }, [item.document.annotatedImage]);
37
+ return url ? _jsx("img", { className: "sketch-change__thumbnail", src: url, alt: "Sketch annotation", "data-test": "sketch-thumbnail" }) : _jsx("div", { className: "sketch-change__thumbnail sketch-change__thumbnail--empty", "data-test": "sketch-thumbnail" });
38
+ }
39
+ function SketchRow({ item }) {
40
+ const [actionError, setActionError] = useState(null);
41
+ const busy = item.status === "dispatching" || item.status === "handing-off";
42
+ const canReview = item.status === "needs-review" || item.status === "unknown";
43
+ const document = item.document;
44
+ async function copyImage() {
45
+ setActionError(null);
46
+ try {
47
+ await copyImageToClipboard(document.annotatedImage);
48
+ }
49
+ catch (error) {
50
+ setActionError(error instanceof Error ? error.message : "The image could not be copied.");
51
+ }
52
+ }
53
+ async function runQueueAction(action, failureMessage) {
54
+ setActionError(null);
55
+ try {
56
+ if (!await action())
57
+ setActionError(failureMessage);
58
+ }
59
+ catch (error) {
60
+ setActionError(error instanceof Error ? error.message : failureMessage);
61
+ }
62
+ }
63
+ return (_jsxs("div", { className: "sketch-change__row", "data-test": "sketch-row", "data-sketch-id": document.id, children: [_jsx(Thumbnail, { item: item }), _jsxs("div", { className: "sketch-change__body", children: [_jsxs("div", { className: "sketch-change__heading", children: [_jsx("span", { className: "sketch-change__name", children: document.filename }), _jsx("span", { className: "sketch-change__status", "data-test": "sketch-status", "data-status": item.status, children: statusLabel(item.status) })] }), _jsxs("span", { className: "sketch-change__route", children: [routeLabel(document.capture.url), " \u00B7 revision ", document.revision] }), document.description ? _jsx("p", { className: "sketch-change__description", children: document.description }) : null, document.annotations?.length ? (_jsx("ol", { className: "sketch-change__annotations", children: document.annotations.map((annotation) => (_jsxs("li", { children: [_jsxs("span", { children: [annotation.number, "."] }), " ", annotation.description] }, annotation.id))) })) : null, _jsxs("div", { className: "sketch-change__actions", children: [_jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-copy-image", onClick: () => void copyImage(), children: [_jsx(IconCopy, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Copy image"] }), _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-download-image", onClick: () => downloadImage(document.annotatedImage, document.filename), children: [_jsx(IconDownload, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Download"] }), _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-reopen", onClick: () => editSketch(document.id), disabled: busy, children: [_jsx(IconEdit, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Reopen"] }), canReview ? (_jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-requeue", onClick: () => void runQueueAction(() => requeueSketch(document.id), "The sketch could not be requeued."), disabled: busy, children: [_jsx(IconRefresh, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " Requeue"] })) : null, _jsxs(Button, { size: "compact", variant: "quiet", type: "button", "data-test": "sketch-mark-done", onClick: () => void runQueueAction(() => removeSketch(document.id), "The sketch could not be removed yet."), disabled: busy, children: [canReview ? _jsx(IconCheck, { size: "var(--icon-size-small)", "aria-hidden": "true" }) : _jsx(IconTrash, { size: "var(--icon-size-small)", "aria-hidden": "true" }), " ", canReview ? "Mark done" : "Delete"] })] }), actionError ? _jsx("span", { className: "sketch-change__error", role: "status", children: actionError }) : null] })] }));
64
+ }
65
+ export function SketchChanges() {
66
+ const store = useSketchStore();
67
+ const items = store.items;
68
+ const hasError = store.error !== null;
69
+ if (items.length === 0 && !hasError)
70
+ return null;
71
+ return (_jsxs("section", { className: "sketch-change", "data-test": "sketches", children: [_jsxs("div", { className: "sketch-change__title", children: ["Sketch annotations ", _jsx("span", { children: items.length })] }), store.error ? _jsx(StatusCallout, { tone: "warning", "data-test": "sketch-storage-error", children: store.error }) : null, items.length > 0 ? items.map((item) => _jsx(SketchRow, { item: item }, `${item.document.id}:${item.document.revision}`)) : (_jsx(StatusCallout, { tone: "warning", children: "Saved sketches need recovery. Reload the page to try again." }))] }));
72
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement } from "react";
2
+ import { type SketchAnnotation, type SketchDocument, type SketchStroke } from "./model.js";
3
+ import type { CapturedSketch } from "./capture.js";
4
+ export interface SketchEditorProps {
5
+ readonly open: boolean;
6
+ readonly document: SketchDocument | null;
7
+ readonly captured: CapturedSketch | null;
8
+ readonly onCancel: () => void;
9
+ readonly onSave: (input: {
10
+ readonly id?: string;
11
+ readonly capture: SketchDocument["capture"];
12
+ readonly description: string;
13
+ readonly strokes: readonly SketchStroke[];
14
+ readonly annotations?: readonly SketchAnnotation[];
15
+ readonly imageWidth: number;
16
+ readonly imageHeight: number;
17
+ readonly originalImage: Blob;
18
+ readonly annotatedImage: Blob;
19
+ }) => Promise<void>;
20
+ }
21
+ export declare function SketchEditor({ open, document: sketchDocument, captured, onCancel, onSave }: SketchEditorProps): ReactElement | null;