nudge-ui 0.2.0 → 0.2.1

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 (201) 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 +872 -12
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +60 -11
  7. package/dist/hosts/next/sidecar.d.ts +4 -0
  8. package/dist/hosts/next/sidecar.js +49 -6
  9. package/dist/hosts/next/wrapper.js +30 -2
  10. package/dist/hosts/projectBridge.d.ts +20 -0
  11. package/dist/hosts/projectBridge.js +97 -0
  12. package/dist/hosts/static/cli.d.ts +1 -1
  13. package/dist/hosts/static/cli.js +6 -1
  14. package/dist/hosts/static/manifest.d.ts +1 -1
  15. package/dist/hosts/static/manifest.js +1 -1
  16. package/dist/hosts/static/server.js +44 -3
  17. package/dist/hosts/vite/vite.d.ts +5 -1
  18. package/dist/hosts/vite/vite.js +78 -14
  19. package/dist/inspector/agent/McpConnectionDialog.d.ts +2 -2
  20. package/dist/inspector/agent/McpConnectionDialog.js +11 -35
  21. package/dist/inspector/agent/client.d.ts +10 -3
  22. package/dist/inspector/agent/client.js +80 -9
  23. package/dist/inspector/agent/connectionStatus.js +3 -3
  24. package/dist/inspector/agent/httpTransport.d.ts +2 -0
  25. package/dist/inspector/agent/httpTransport.js +37 -17
  26. package/dist/inspector/agent/protocol.d.ts +8 -3
  27. package/dist/inspector/agent/protocol.js +1 -1
  28. package/dist/inspector/agent/verification.d.ts +1 -1
  29. package/dist/inspector/agent/verification.js +69 -12
  30. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  31. package/dist/inspector/canvas/CanvasCard.js +181 -91
  32. package/dist/inspector/canvas/CanvasElementOverlay.js +246 -52
  33. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  35. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  36. package/dist/inspector/canvas/CanvasWorkspace.js +392 -73
  37. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  38. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  39. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  40. package/dist/inspector/canvas/canvasResize.js +52 -0
  41. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  42. package/dist/inspector/canvas/canvasStore.js +128 -61
  43. package/dist/inspector/canvas/frameProtocol.d.ts +69 -5
  44. package/dist/inspector/canvas/frameProtocol.js +56 -2
  45. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -0
  46. package/dist/inspector/canvas/keyboardShortcuts.js +14 -0
  47. package/dist/inspector/canvas/projection.d.ts +7 -0
  48. package/dist/inspector/canvas/projection.js +118 -25
  49. package/dist/inspector/canvas/rendererBootstrap.js +29 -29
  50. package/dist/inspector/canvas/rendererElementSelector.js +341 -30
  51. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  52. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  53. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  54. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  55. package/dist/inspector/canvas/sessionStore.js +12 -4
  56. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  57. package/dist/inspector/changes/changesLog.js +31 -1
  58. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  59. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  60. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  61. package/dist/inspector/clientManifest.d.ts +5 -0
  62. package/dist/inspector/clientManifest.js +11 -0
  63. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  64. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  65. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  66. package/dist/inspector/index.d.ts +2 -0
  67. package/dist/inspector/index.js +74 -24
  68. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  69. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  70. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  71. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  72. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  73. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  74. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  75. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  76. package/dist/inspector/inline-text/textEditChange.js +165 -0
  77. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  78. package/dist/inspector/inspection/selectionProperty.js +1 -0
  79. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  80. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  81. package/dist/inspector/overlay/spacingGestures.js +353 -0
  82. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  83. package/dist/inspector/overlay/structuralGestures.js +12 -10
  84. package/dist/inspector/projection/managedStylesheet.js +4 -1
  85. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  86. package/dist/inspector/projection/renderedInstance.js +5 -1
  87. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  88. package/dist/inspector/projection/structuralProjection.js +85 -1
  89. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  90. package/dist/inspector/projection/textProjection.d.ts +2 -0
  91. package/dist/inspector/projection/textProjection.js +10 -4
  92. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  93. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  94. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  95. package/dist/inspector/prompt/generatePrompt.js +19 -2
  96. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  97. package/dist/inspector/runtime/editorShell.js +4 -0
  98. package/dist/inspector/runtime/runtimeConfig.d.ts +4 -0
  99. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  100. package/dist/inspector/selection/selectionStore.js +2 -0
  101. package/dist/inspector/settings/SettingsDialog.js +10 -0
  102. package/dist/inspector/shell/AppShell.js +5 -1
  103. package/dist/inspector/shell/ChangesLog.js +8 -4
  104. package/dist/inspector/shell/CopyPromptButton.js +95 -19
  105. package/dist/inspector/shell/EmptyState.js +1 -1
  106. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  107. package/dist/inspector/shell/InspectorShell.js +37 -38
  108. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  109. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  110. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  111. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  112. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  113. package/dist/inspector/shell/originalPreview.js +61 -0
  114. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  115. package/dist/inspector/shell/shortcuts.js +15 -0
  116. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  117. package/dist/inspector/sketch/SketchChanges.js +72 -0
  118. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  119. package/dist/inspector/sketch/SketchEditor.js +402 -0
  120. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  121. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  122. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  123. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  124. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  125. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  126. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  127. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  128. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  129. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  130. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  131. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  132. package/dist/inspector/sketch/capture.d.ts +55 -0
  133. package/dist/inspector/sketch/capture.js +350 -0
  134. package/dist/inspector/sketch/freehand.d.ts +13 -0
  135. package/dist/inspector/sketch/freehand.js +64 -0
  136. package/dist/inspector/sketch/geometry.d.ts +17 -0
  137. package/dist/inspector/sketch/geometry.js +98 -0
  138. package/dist/inspector/sketch/handoff.d.ts +21 -0
  139. package/dist/inspector/sketch/handoff.js +72 -0
  140. package/dist/inspector/sketch/interaction.d.ts +21 -0
  141. package/dist/inspector/sketch/interaction.js +98 -0
  142. package/dist/inspector/sketch/model.d.ts +104 -0
  143. package/dist/inspector/sketch/model.js +282 -0
  144. package/dist/inspector/sketch/persistence.d.ts +21 -0
  145. package/dist/inspector/sketch/persistence.js +205 -0
  146. package/dist/inspector/sketch/raster.d.ts +22 -0
  147. package/dist/inspector/sketch/raster.js +203 -0
  148. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  149. package/dist/inspector/sketch/sketchNote.js +30 -0
  150. package/dist/inspector/sketch/store.d.ts +47 -0
  151. package/dist/inspector/sketch/store.js +409 -0
  152. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  153. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  154. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  155. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  156. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  157. package/dist/inspector/styleEditors/GapField.js +2 -2
  158. package/dist/inspector/styleEditors/GridSection.js +1 -1
  159. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  160. package/dist/inspector/styleEditors/SpacingBox.js +23 -30
  161. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  162. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  163. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  164. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  165. package/dist/inspector/styleEditors/nudgeValue.js +91 -5
  166. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  167. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  168. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  169. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  170. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  171. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  172. package/dist/inspector/tokens/TokenField.js +111 -7
  173. package/dist/inspector/tokens/editActions.d.ts +2 -0
  174. package/dist/inspector/tokens/editActions.js +2 -2
  175. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  176. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  177. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  178. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  179. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  180. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  181. package/dist/inspector/tokens/resolution.d.ts +3 -26
  182. package/dist/inspector/tokens/resolution.js +44 -735
  183. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  184. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  185. package/dist/inspector/ui/Button.d.ts +1 -1
  186. package/dist/inspector/ui/SideValuesField.js +2 -2
  187. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  188. package/dist/inspector/ui/StatusCallout.js +2 -2
  189. package/dist/inspector/ui/styles.js +13 -11
  190. package/dist/internal/agent-protocol.d.ts +65 -1
  191. package/dist/internal/agent-protocol.js +183 -3
  192. package/dist/nudge-ui.mjs +261 -57
  193. package/dist/transport/editor.d.ts +28 -0
  194. package/dist/transport/editor.js +132 -0
  195. package/dist/transport/index.d.ts +1 -0
  196. package/dist/transport/index.js +1 -0
  197. package/dist/transport/routes.d.ts +7 -0
  198. package/dist/transport/routes.js +7 -0
  199. package/package.json +3 -1
  200. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  201. package/dist/inspector/shell/panelLayout.js +0 -29
@@ -0,0 +1,165 @@
1
+ import { createComponentPropChange } from "../componentSemantics/changeModel.js";
2
+ import { captureTextProjectionTarget, textProjectionSelector } from "../projection/textProjection.js";
3
+ export function initialTextEditDecision(candidate) {
4
+ return {
5
+ bindingIndex: candidate.bindingChoices?.length === 1 ? 0 : null,
6
+ scope: candidate.scope ?? (candidate.binding.kind === "rendered-text" ? "rendered-instance" : "source-site"),
7
+ };
8
+ }
9
+ export function chooseTextEditBinding(candidate, index) {
10
+ const choice = candidate.bindingChoices?.[index];
11
+ if (!choice)
12
+ return null;
13
+ return {
14
+ bindingIndex: index,
15
+ scope: choice.mountedCount > 1 ? "rendered-instance" : "source-site",
16
+ };
17
+ }
18
+ function selectedBinding(candidate, decision) {
19
+ return decision.bindingIndex === null ? null : candidate.bindingChoices?.[decision.bindingIndex] ?? null;
20
+ }
21
+ export function getTextEditScopeChoices(candidate, decision) {
22
+ const choice = selectedBinding(candidate, decision);
23
+ if (!choice)
24
+ return candidate.scopeChoices ?? [];
25
+ return choice.mountedCount > 1 && choice.authoredAs === "literal"
26
+ ? ["rendered-instance", "source-site"]
27
+ : [];
28
+ }
29
+ export function getTextEditBinding(candidate, decision, capturedTarget) {
30
+ const choice = selectedBinding(candidate, decision);
31
+ if (!choice)
32
+ return candidate.binding;
33
+ if (choice.mountedCount > 1 && choice.authoredAs !== "literal") {
34
+ const target = choice.renderedTarget ?? candidate.renderedTarget ?? capturedTarget;
35
+ return target ? { kind: "rendered-text", target } : choice.binding;
36
+ }
37
+ return choice.binding;
38
+ }
39
+ function sourceForChoice(candidate, choice) {
40
+ if (choice?.renderedSource)
41
+ return choice.renderedSource;
42
+ if (candidate.renderedSource
43
+ && (!choice || candidate.renderedSource.evidence?.callsiteId === choice.binding.target.callsiteId
44
+ && candidate.renderedSource.evidence.property === choice.binding.property)) {
45
+ return candidate.renderedSource;
46
+ }
47
+ const parsed = /^(.*):(\d+):(\d+)$/.exec(candidate.element.getAttribute("data-src") ?? "");
48
+ const target = choice?.binding.target;
49
+ const authoredAs = choice?.authoredAs === "literal"
50
+ ? "literal"
51
+ : choice?.authoredAs === "expression" || choice?.authoredAs === "spread"
52
+ ? "expression"
53
+ : "unknown";
54
+ return {
55
+ file: target?.file ?? parsed?.[1] ?? candidate.element.getAttribute("data-src") ?? "",
56
+ line: target?.line ?? Number(parsed?.[2] ?? 0),
57
+ column: target?.column ?? Number(parsed?.[3] ?? 0),
58
+ component: target?.componentName ?? candidate.element.getAttribute("data-cid") ?? "Rendered text",
59
+ selector: candidate.binding.kind === "rendered-text"
60
+ ? textProjectionSelector(candidate.binding.target) ?? ""
61
+ : "",
62
+ authoredAs,
63
+ evidence: choice ? {
64
+ callsiteId: choice.binding.target.callsiteId,
65
+ componentName: choice.binding.target.componentName,
66
+ property: choice.binding.property,
67
+ mountedCount: choice.mountedCount,
68
+ } : undefined,
69
+ };
70
+ }
71
+ function componentEvidence(candidate, choice) {
72
+ const target = choice.renderedTarget
73
+ ?? candidate.renderedTarget
74
+ ?? captureTextProjectionTarget(candidate.element, candidate.before, candidate.textNode);
75
+ return {
76
+ occurrence: target?.occurrence ?? 0,
77
+ props: target?.props ?? candidate.element.getAttribute("data-cprops"),
78
+ ariaLabel: target?.ariaLabel ?? candidate.element.getAttribute("aria-label"),
79
+ beforeText: candidate.before,
80
+ mountedCount: choice.mountedCount,
81
+ };
82
+ }
83
+ function textTargetFor(candidate, binding, choice) {
84
+ if (choice?.renderedTarget)
85
+ return choice.renderedTarget;
86
+ if (candidate.renderedTarget)
87
+ return candidate.renderedTarget;
88
+ if (binding.kind === "rendered-text")
89
+ return binding.target;
90
+ return captureTextProjectionTarget(candidate.element, candidate.before, candidate.textNode);
91
+ }
92
+ function choiceForCommit(candidate, selectedChoice) {
93
+ const choice = selectedChoice ?? candidate.bindingChoices?.[0];
94
+ const target = candidate.editableTarget;
95
+ if (choice || !target || candidate.mountedCount === undefined)
96
+ return choice ?? null;
97
+ const property = candidate.binding.kind === "component-prop"
98
+ ? candidate.binding.property
99
+ : "children";
100
+ return {
101
+ binding: candidate.binding.kind === "component-prop"
102
+ ? candidate.binding
103
+ : {
104
+ kind: "component-prop",
105
+ property,
106
+ target: {
107
+ framework: target.framework,
108
+ componentId: target.contract.componentId,
109
+ callsiteId: target.meta.callsiteId,
110
+ componentName: target.meta.componentName,
111
+ file: target.meta.file,
112
+ line: target.meta.line,
113
+ column: target.meta.column,
114
+ },
115
+ },
116
+ editableTarget: target,
117
+ mountedCount: candidate.mountedCount,
118
+ authoredAs: target.meta.authoredProps[property] ?? "default",
119
+ renderedTarget: candidate.renderedTarget ?? null,
120
+ renderedSource: candidate.renderedSource,
121
+ };
122
+ }
123
+ /** Builds a canonical record without changing the DOM, appending history, or ending a draft. */
124
+ export function prepareTextEditChange(candidate, decision, capturedTarget, after) {
125
+ if ((candidate.bindingChoices?.length ?? 0) > 1 && decision.bindingIndex === null)
126
+ return null;
127
+ const binding = getTextEditBinding(candidate, decision, capturedTarget);
128
+ const choice = choiceForCommit(candidate, selectedBinding(candidate, decision));
129
+ if (binding.kind === "component-prop" && decision.scope === "source-site") {
130
+ const editableTarget = choice?.editableTarget ?? candidate.editableTarget;
131
+ const prop = editableTarget?.contract.props.find((item) => item.name === binding.property && item.control === "text");
132
+ if (!editableTarget || !prop)
133
+ return null;
134
+ return createComponentPropChange(editableTarget, prop, after, {
135
+ scope: "source-site",
136
+ evidence: choice && choice.mountedCount > 1
137
+ ? componentEvidence(candidate, choice)
138
+ : undefined,
139
+ });
140
+ }
141
+ const textTarget = textTargetFor(candidate, binding, choice);
142
+ const source = sourceForChoice(candidate, choice);
143
+ const renderedSource = source.selector || !textTarget
144
+ ? source
145
+ : { ...source, selector: textProjectionSelector(textTarget) ?? "" };
146
+ if (!textTarget || !renderedSource.selector)
147
+ return null;
148
+ return {
149
+ kind: "text-content",
150
+ id: `text-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`,
151
+ target: textTarget,
152
+ source: {
153
+ file: renderedSource.file,
154
+ line: renderedSource.line,
155
+ column: renderedSource.column,
156
+ component: renderedSource.component,
157
+ },
158
+ selector: renderedSource.selector,
159
+ before: candidate.before,
160
+ after,
161
+ authoredAs: renderedSource.authoredAs,
162
+ scope: decision.scope,
163
+ evidence: renderedSource.evidence,
164
+ };
165
+ }
@@ -19,6 +19,7 @@ export interface SelectionProperty {
19
19
  value: SelectionValue;
20
20
  token: SelectionToken;
21
21
  primaryRow: ResolvedProperty | null;
22
+ rows: readonly (ResolvedProperty | null)[];
22
23
  }
23
24
  /**
24
25
  * Projects browser values and authored inspection rows into group-safe facts.
@@ -22,6 +22,7 @@ export function projectSelectionProperty(property, rows, values, primaryIndex) {
22
22
  value: common ? { kind: "common", value: first } : { kind: "mixed" },
23
23
  token: tokenFor(rows),
24
24
  primaryRow: rows[primaryIndex] ?? null,
25
+ rows,
25
26
  };
26
27
  }
27
28
  /** Returns the token entries available to every inspected target. */
@@ -20,6 +20,24 @@ export function observeSelectedGeometry(element, onChange) {
20
20
  onChange();
21
21
  });
22
22
  }
23
+ let scrollTargets = [];
24
+ function unbindScrollTargets() {
25
+ for (const target of scrollTargets)
26
+ target.removeEventListener("scroll", schedule, true);
27
+ scrollTargets = [];
28
+ }
29
+ function bindScrollTargets() {
30
+ unbindScrollTargets();
31
+ const targets = [view, view.document];
32
+ let ancestor = element.parentElement;
33
+ while (ancestor) {
34
+ targets.push(ancestor);
35
+ ancestor = ancestor.parentElement;
36
+ }
37
+ scrollTargets = [...new Set(targets)];
38
+ for (const target of scrollTargets)
39
+ target.addEventListener("scroll", schedule, true);
40
+ }
23
41
  const resizeObserver = ResizeObserverCtor
24
42
  ? new ResizeObserverCtor(schedule)
25
43
  : null;
@@ -32,6 +50,7 @@ export function observeSelectedGeometry(element, onChange) {
32
50
  mutationObserver?.disconnect();
33
51
  if (observedParent)
34
52
  mutationObserver?.observe(observedParent, { childList: true });
53
+ bindScrollTargets();
35
54
  }
36
55
  schedule();
37
56
  }) : null;
@@ -39,13 +58,13 @@ export function observeSelectedGeometry(element, onChange) {
39
58
  if (observedParent)
40
59
  mutationObserver?.observe(observedParent, { childList: true });
41
60
  view.addEventListener("resize", schedule);
42
- view.addEventListener("scroll", schedule, true);
61
+ bindScrollTargets();
43
62
  return () => {
44
63
  if (frame !== null)
45
64
  view.cancelAnimationFrame(frame);
46
65
  resizeObserver?.disconnect();
47
66
  mutationObserver?.disconnect();
48
67
  view.removeEventListener("resize", schedule);
49
- view.removeEventListener("scroll", schedule, true);
68
+ unbindScrollTargets();
50
69
  };
51
70
  }
@@ -0,0 +1,45 @@
1
+ import type { Rect } from "./overlayGeometry.js";
2
+ export type PaddingSide = "top" | "right" | "bottom" | "left";
3
+ export type SpacingKind = "padding" | "gap";
4
+ export type SpacingAxis = "horizontal" | "vertical";
5
+ export type SpacingDragAxis = "x" | "y";
6
+ export type SpacingProperty = `padding-${PaddingSide}` | "row-gap" | "column-gap";
7
+ export interface SpacingGuideData {
8
+ kind: SpacingKind;
9
+ property: SpacingProperty;
10
+ side: PaddingSide | null;
11
+ axis: SpacingAxis;
12
+ dragAxis: SpacingDragAxis;
13
+ value: number;
14
+ direction: 1 | -1;
15
+ cursor: "ew-resize" | "ns-resize";
16
+ guide: Rect;
17
+ affectedGuides: Rect[];
18
+ affectedAreas: Rect[];
19
+ /** A 24px-thick pointer target centred on the visible, up-to-40px guide handle. */
20
+ hit: Rect;
21
+ }
22
+ export interface SpacingAffordance extends SpacingGuideData {
23
+ element: HTMLElement;
24
+ }
25
+ export interface SpacingDescriptor {
26
+ kind: SpacingKind;
27
+ property: SpacingProperty;
28
+ side: PaddingSide | null;
29
+ }
30
+ export declare const SPACING_GUIDE_HANDLE_LENGTH = 40;
31
+ /** Finds the spacing target under a document-local pointer location. */
32
+ export declare function getSpacingAffordanceAtPoint(doc: Document, x: number, y: number): SpacingAffordance | null;
33
+ /** Re-resolves a renderer-reported spacing descriptor in its owner document. */
34
+ export declare function getSpacingAffordanceForDescriptor(element: HTMLElement, descriptor: SpacingDescriptor): SpacingAffordance | null;
35
+ /** Calculates the pixel value represented by a direct-manipulation drag. */
36
+ export declare function spacingValueForDrag(affordance: Pick<SpacingAffordance, "dragAxis" | "direction" | "value">, start: {
37
+ x: number;
38
+ y: number;
39
+ }, point: {
40
+ x: number;
41
+ y: number;
42
+ }): number;
43
+ export declare function spacingValueCss(value: number): string;
44
+ /** Serializes the stable part of an affordance for renderer-to-controller messages. */
45
+ export declare function toSpacingDescriptor(affordance: SpacingAffordance): SpacingDescriptor;
@@ -0,0 +1,353 @@
1
+ import { getElementComputedStyle } from "../runtime/domRealm.js";
2
+ const SPACING_DRAG_TARGET_SIZE = 24;
3
+ export const SPACING_GUIDE_HANDLE_LENGTH = 40;
4
+ const EPSILON = 0.01;
5
+ function rightOf(rect) {
6
+ return Number.isFinite(rect.right) ? rect.right : rect.left + rect.width;
7
+ }
8
+ function bottomOf(rect) {
9
+ return Number.isFinite(rect.bottom) ? rect.bottom : rect.top + rect.height;
10
+ }
11
+ function boxEdges(element) {
12
+ const rect = element.getBoundingClientRect();
13
+ return {
14
+ left: rect.left,
15
+ top: rect.top,
16
+ right: rightOf(rect),
17
+ bottom: bottomOf(rect),
18
+ };
19
+ }
20
+ // Computed style resolves to px in browsers, so drag math normalizes to px.
21
+ // An authored rem/% is intentionally rewritten as px on commit.
22
+ function numberValue(value) {
23
+ const parsed = Number.parseFloat(value);
24
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
25
+ }
26
+ function boxModel(element) {
27
+ const style = getElementComputedStyle(element);
28
+ const padding = {
29
+ top: numberValue(style.paddingTop),
30
+ right: numberValue(style.paddingRight),
31
+ bottom: numberValue(style.paddingBottom),
32
+ left: numberValue(style.paddingLeft),
33
+ };
34
+ const border = {
35
+ top: numberValue(style.borderTopWidth),
36
+ right: numberValue(style.borderRightWidth),
37
+ bottom: numberValue(style.borderBottomWidth),
38
+ left: numberValue(style.borderLeftWidth),
39
+ };
40
+ const outer = boxEdges(element);
41
+ const inner = {
42
+ left: outer.left + border.left,
43
+ top: outer.top + border.top,
44
+ right: outer.right - border.right,
45
+ bottom: outer.bottom - border.bottom,
46
+ };
47
+ const content = {
48
+ left: inner.left + padding.left,
49
+ top: inner.top + padding.top,
50
+ right: inner.right - padding.right,
51
+ bottom: inner.bottom - padding.bottom,
52
+ };
53
+ return { outer, inner, content, padding };
54
+ }
55
+ function containsPoint(rect, x, y) {
56
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
57
+ }
58
+ function containsRectPoint(rect, x, y) {
59
+ return x >= rect.left
60
+ && x <= rect.left + rect.width
61
+ && y >= rect.top
62
+ && y <= rect.top + rect.height;
63
+ }
64
+ function guideHit(guide, orientation) {
65
+ if (orientation === "horizontal") {
66
+ const height = Math.max(SPACING_DRAG_TARGET_SIZE, guide.height);
67
+ const width = Math.min(SPACING_GUIDE_HANDLE_LENGTH, guide.width);
68
+ return {
69
+ left: guide.left + (guide.width - width) / 2,
70
+ top: guide.top - (height - guide.height) / 2,
71
+ width,
72
+ height,
73
+ };
74
+ }
75
+ const width = Math.max(SPACING_DRAG_TARGET_SIZE, guide.width);
76
+ const height = Math.min(SPACING_GUIDE_HANDLE_LENGTH, guide.height);
77
+ return {
78
+ left: guide.left - (width - guide.width) / 2,
79
+ top: guide.top + (guide.height - height) / 2,
80
+ width,
81
+ height,
82
+ };
83
+ }
84
+ const PADDING_CONFIG = {
85
+ top: { axis: "horizontal", dragAxis: "y", direction: 1 },
86
+ right: { axis: "vertical", dragAxis: "x", direction: 1 },
87
+ bottom: { axis: "horizontal", dragAxis: "y", direction: 1 },
88
+ left: { axis: "vertical", dragAxis: "x", direction: 1 },
89
+ };
90
+ function paddingAffordanceForSide(element, side, allowZero, model = boxModel(element)) {
91
+ const { inner, content, padding } = model;
92
+ const value = padding[side];
93
+ if (!allowZero && value <= EPSILON)
94
+ return null;
95
+ const config = PADDING_CONFIG[side];
96
+ const width = Math.max(0, inner.right - inner.left);
97
+ const height = Math.max(0, inner.bottom - inner.top);
98
+ const guide = config.axis === "horizontal"
99
+ ? {
100
+ left: inner.left,
101
+ top: (side === "top" ? inner.top + value / 2 : inner.bottom - value / 2) - 1,
102
+ width,
103
+ height: 2,
104
+ }
105
+ : {
106
+ left: (side === "left" ? inner.left + value / 2 : inner.right - value / 2) - 1,
107
+ top: inner.top,
108
+ width: 2,
109
+ height,
110
+ };
111
+ const area = config.axis === "horizontal"
112
+ ? {
113
+ left: inner.left,
114
+ top: side === "top" ? inner.top : content.bottom,
115
+ width,
116
+ height: Math.max(0, (side === "top" ? content.top : inner.bottom) - (side === "top" ? inner.top : content.bottom)),
117
+ }
118
+ : {
119
+ left: side === "left" ? inner.left : content.right,
120
+ top: inner.top,
121
+ width: Math.max(0, (side === "left" ? content.left : inner.right) - (side === "left" ? inner.left : content.right)),
122
+ height,
123
+ };
124
+ const hit = guideHit(guide, config.axis);
125
+ return {
126
+ kind: "padding",
127
+ property: `padding-${side}`,
128
+ side,
129
+ axis: config.axis,
130
+ dragAxis: config.dragAxis,
131
+ value,
132
+ cursor: config.axis === "horizontal" ? "ns-resize" : "ew-resize",
133
+ guide,
134
+ affectedGuides: [guide],
135
+ affectedAreas: [area],
136
+ hit,
137
+ direction: config.direction,
138
+ element,
139
+ };
140
+ }
141
+ function paddingAffordanceAtPoint(element, x, y, allowZero) {
142
+ const model = boxModel(element);
143
+ if (!containsPoint(model.outer, x, y))
144
+ return null;
145
+ const sides = ["top", "right", "bottom", "left"];
146
+ for (const side of sides) {
147
+ const affordance = paddingAffordanceForSide(element, side, allowZero, model);
148
+ if (affordance && containsRectPoint(affordance.hit, x, y))
149
+ return affordance;
150
+ }
151
+ return null;
152
+ }
153
+ function overlap(startA, endA, startB, endB) {
154
+ return Math.min(endA, endB) - Math.max(startA, startB) > EPSILON;
155
+ }
156
+ function layoutChildren(element) {
157
+ const HTMLElementConstructor = element.ownerDocument.defaultView?.HTMLElement;
158
+ if (!HTMLElementConstructor)
159
+ return [];
160
+ return Array.from(element.children)
161
+ .filter((child) => child instanceof HTMLElementConstructor)
162
+ .map((child) => ({ element: child, rect: boxEdges(child) }));
163
+ }
164
+ function gapSegments(element) {
165
+ const style = getElementComputedStyle(element);
166
+ if (style.display !== "flex"
167
+ && style.display !== "inline-flex"
168
+ && style.display !== "grid"
169
+ && style.display !== "inline-grid")
170
+ return [];
171
+ const children = layoutChildren(element);
172
+ const segments = [];
173
+ const byLeft = [...children].sort((left, right) => left.rect.left - right.rect.left);
174
+ const byTop = [...children].sort((top, bottom) => top.rect.top - bottom.rect.top);
175
+ for (const left of byLeft) {
176
+ const next = byLeft.find((right) => right.rect.left >= left.rect.right - EPSILON
177
+ && overlap(left.rect.top, left.rect.bottom, right.rect.top, right.rect.bottom));
178
+ if (!next)
179
+ continue;
180
+ segments.push({
181
+ property: "column-gap",
182
+ axis: "horizontal",
183
+ start: left.rect.right,
184
+ end: next.rect.left,
185
+ crossStart: Math.max(left.rect.top, next.rect.top),
186
+ crossEnd: Math.min(left.rect.bottom, next.rect.bottom),
187
+ });
188
+ }
189
+ for (const top of byTop) {
190
+ const next = byTop.find((bottom) => bottom.rect.top >= top.rect.bottom - EPSILON
191
+ && overlap(top.rect.left, top.rect.right, bottom.rect.left, bottom.rect.right));
192
+ if (!next)
193
+ continue;
194
+ segments.push({
195
+ property: "row-gap",
196
+ axis: "vertical",
197
+ start: top.rect.bottom,
198
+ end: next.rect.top,
199
+ crossStart: Math.max(top.rect.left, next.rect.left),
200
+ crossEnd: Math.min(top.rect.right, next.rect.right),
201
+ });
202
+ }
203
+ return segments;
204
+ }
205
+ function gapAffordanceAtPoint(element, x, y, property) {
206
+ const segments = gapSegments(element);
207
+ if (segments.length === 0)
208
+ return null;
209
+ const style = getElementComputedStyle(element);
210
+ const candidate = segments.find((segment) => {
211
+ if (property && segment.property !== property)
212
+ return false;
213
+ return containsRectPoint(gapDragHit(segment), x, y);
214
+ });
215
+ if (!candidate)
216
+ return null;
217
+ const value = gapValue(style, candidate.property);
218
+ const guide = gapGuide(candidate);
219
+ const matchingSegments = segments.filter((segment) => segment.property === candidate.property);
220
+ const hit = gapDragHit(candidate);
221
+ return {
222
+ kind: "gap",
223
+ property: candidate.property,
224
+ side: null,
225
+ axis: candidate.axis,
226
+ dragAxis: candidate.axis === "horizontal" ? "x" : "y",
227
+ value,
228
+ cursor: candidate.axis === "horizontal" ? "ew-resize" : "ns-resize",
229
+ guide,
230
+ affectedGuides: matchingSegments.map(gapGuide),
231
+ affectedAreas: matchingSegments.map(gapArea),
232
+ hit,
233
+ // Gap size is absolute screen space; dragging along +axis always grows it.
234
+ direction: 1,
235
+ element,
236
+ };
237
+ }
238
+ function gapGuide(segment) {
239
+ const midpoint = (segment.start + segment.end) / 2;
240
+ const crossSize = Math.max(0, segment.crossEnd - segment.crossStart);
241
+ return segment.axis === "horizontal"
242
+ ? { left: midpoint - 1, top: segment.crossStart, width: 2, height: crossSize }
243
+ : { left: segment.crossStart, top: midpoint - 1, width: crossSize, height: 2 };
244
+ }
245
+ function gapArea(segment) {
246
+ const crossSize = Math.max(0, segment.crossEnd - segment.crossStart);
247
+ return segment.axis === "horizontal"
248
+ ? {
249
+ left: segment.start,
250
+ top: segment.crossStart,
251
+ width: Math.max(0, segment.end - segment.start),
252
+ height: crossSize,
253
+ }
254
+ : {
255
+ left: segment.crossStart,
256
+ top: segment.start,
257
+ width: crossSize,
258
+ height: Math.max(0, segment.end - segment.start),
259
+ };
260
+ }
261
+ function gapDragHit(segment) {
262
+ const orientation = segment.axis === "horizontal" ? "vertical" : "horizontal";
263
+ return guideHit(gapGuide(segment), orientation);
264
+ }
265
+ function gapValue(style, property) {
266
+ const direct = style.getPropertyValue(property)
267
+ || (property === "row-gap" ? style.rowGap : style.columnGap);
268
+ if (direct.trim())
269
+ return numberValue(direct);
270
+ // jsdom and a few older browser CSSOM implementations expose `gap` but not
271
+ // the longhands through getPropertyValue(). Keep the gesture model aligned
272
+ // with the browser's shorthand fallback in those realms.
273
+ const shorthand = style.getPropertyValue("gap") || style.gap;
274
+ const values = shorthand.trim().split(/\s+/).filter(Boolean);
275
+ if (values.length === 0)
276
+ return 0;
277
+ if (values.length === 1)
278
+ return numberValue(values[0]);
279
+ return numberValue(property === "row-gap" ? values[0] : values[1]);
280
+ }
281
+ function trackedElementsAtPoint(doc, x, y) {
282
+ if (typeof doc.elementFromPoint !== "function")
283
+ return [];
284
+ const target = doc.elementFromPoint(x, y);
285
+ if (!target)
286
+ return [];
287
+ const HTMLElementConstructor = doc.defaultView?.HTMLElement;
288
+ if (!HTMLElementConstructor)
289
+ return [];
290
+ const result = [];
291
+ let element = target;
292
+ while (element) {
293
+ if (element instanceof HTMLElementConstructor && element.hasAttribute("data-cid")) {
294
+ result.push(element);
295
+ }
296
+ element = element.parentElement;
297
+ }
298
+ return result;
299
+ }
300
+ /** Finds the spacing target under a document-local pointer location. */
301
+ export function getSpacingAffordanceAtPoint(doc, x, y) {
302
+ const candidates = trackedElementsAtPoint(doc, x, y);
303
+ for (const element of candidates) {
304
+ const padding = paddingAffordanceAtPoint(element, x, y, false);
305
+ if (padding)
306
+ return padding;
307
+ }
308
+ for (const element of candidates) {
309
+ const gap = gapAffordanceAtPoint(element, x, y);
310
+ if (gap)
311
+ return gap;
312
+ }
313
+ // A zero-sized padding region still needs a small draggable edge so users
314
+ // can establish padding without first typing a value in the inspector.
315
+ for (const element of candidates) {
316
+ const padding = paddingAffordanceAtPoint(element, x, y, true);
317
+ if (padding)
318
+ return padding;
319
+ }
320
+ return null;
321
+ }
322
+ /** Re-resolves a renderer-reported spacing descriptor in its owner document. */
323
+ export function getSpacingAffordanceForDescriptor(element, descriptor) {
324
+ if (descriptor.kind === "padding") {
325
+ const side = descriptor.side;
326
+ if (!side || descriptor.property !== `padding-${side}`)
327
+ return null;
328
+ return paddingAffordanceForSide(element, side, true);
329
+ }
330
+ if (descriptor.property !== "row-gap" && descriptor.property !== "column-gap")
331
+ return null;
332
+ const segment = gapSegments(element).find((candidate) => candidate.property === descriptor.property);
333
+ if (!segment)
334
+ return null;
335
+ const guide = gapGuide(segment);
336
+ return gapAffordanceAtPoint(element, guide.left + guide.width / 2, guide.top + guide.height / 2, descriptor.property);
337
+ }
338
+ /** Calculates the pixel value represented by a direct-manipulation drag. */
339
+ export function spacingValueForDrag(affordance, start, point) {
340
+ const delta = affordance.dragAxis === "x" ? point.x - start.x : point.y - start.y;
341
+ return Math.max(0, Math.round(affordance.value + delta * affordance.direction));
342
+ }
343
+ export function spacingValueCss(value) {
344
+ return `${Math.max(0, Math.round(value))}px`;
345
+ }
346
+ /** Serializes the stable part of an affordance for renderer-to-controller messages. */
347
+ export function toSpacingDescriptor(affordance) {
348
+ return {
349
+ kind: affordance.kind,
350
+ property: affordance.property,
351
+ side: affordance.side,
352
+ };
353
+ }
@@ -20,9 +20,9 @@ export declare function canContainElement(parent: HTMLElement, child: HTMLElemen
20
20
  /** Derives a safe insertion point; its nodes never leave this gesture module. */
21
21
  export declare function getDropLocationForElement(dragged: HTMLElement, target: HTMLElement, beforeTarget: boolean, preferContainer?: boolean): DropLocation | null;
22
22
  export declare function getDropLocationAtPoint(doc: Document, dragged: HTMLElement, x: number, y: number): DropLocation | null;
23
- /** Capture the move as canonical intent, then project only the controller host. */
23
+ /** Capture the move as canonical intent, then preview it in an inline host. */
24
24
  export declare function moveElement(node: HTMLElement, destination: DropLocation): StructuralMove | null;
25
25
  /** Reorder only same-parent siblings; horizontal movement needs a flex row. */
26
26
  export declare function nudgeElement(node: HTMLElement, key: StructuralNudgeKey): StructuralMove | null;
27
- /** Capture delete intent and project only the controller host. */
27
+ /** Capture delete intent, then preview it in an inline host. */
28
28
  export declare function deleteElement(selected: SelectedElement): StructuralDelete | null;