nudge-ui 0.2.3 → 0.2.5

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 (264) hide show
  1. package/dist/cli/agentCommand.js +4 -28
  2. package/dist/client.mjs +870 -212
  3. package/dist/hosts/next/wrapper.js +1 -7
  4. package/dist/hosts/projectBridge.js +3 -17
  5. package/dist/inspector/agent/McpConnectionDialog.js +7 -3
  6. package/dist/inspector/agent/activity.d.ts +1 -1
  7. package/dist/inspector/agent/activity.js +3 -2
  8. package/dist/inspector/agent/client.d.ts +2 -2
  9. package/dist/inspector/agent/client.js +28 -29
  10. package/dist/inspector/agent/connectionStatus.d.ts +3 -1
  11. package/dist/inspector/agent/connectionStatus.js +25 -0
  12. package/dist/inspector/agent/httpTransport.js +31 -147
  13. package/dist/inspector/agent/protocol.d.ts +1 -1
  14. package/dist/inspector/agent/protocol.js +1 -1
  15. package/dist/inspector/agent/verification.d.ts +5 -7
  16. package/dist/inspector/agent/verification.js +67 -20
  17. package/dist/inspector/boot.d.ts +3 -0
  18. package/dist/inspector/boot.js +297 -0
  19. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +2 -1
  20. package/dist/inspector/canvas/AgentActivityOverlay.js +6 -3
  21. package/dist/inspector/canvas/CanvasCard.js +23 -12
  22. package/dist/inspector/canvas/CanvasElementOverlay.js +97 -128
  23. package/dist/inspector/canvas/CanvasToolbar.js +2 -1
  24. package/dist/inspector/canvas/CanvasWorkspace.js +119 -162
  25. package/dist/inspector/canvas/LockedWorkspaceNotice.d.ts +3 -2
  26. package/dist/inspector/canvas/LockedWorkspaceNotice.js +10 -6
  27. package/dist/inspector/canvas/PageCard.d.ts +10 -0
  28. package/dist/inspector/canvas/PageCard.js +51 -0
  29. package/dist/inspector/canvas/SketchToolOptions.d.ts +5 -0
  30. package/dist/inspector/canvas/SketchToolOptions.js +86 -0
  31. package/dist/inspector/canvas/StaleChangeIndicator.d.ts +1 -1
  32. package/dist/inspector/canvas/StaleChangeIndicator.js +1 -1
  33. package/dist/inspector/canvas/ToolOptionsDrawer.d.ts +21 -0
  34. package/dist/inspector/canvas/ToolOptionsDrawer.js +77 -0
  35. package/dist/inspector/canvas/activeCanvasDocument.d.ts +3 -2
  36. package/dist/inspector/canvas/assets/eraser.svg +28 -0
  37. package/dist/inspector/canvas/assets/pen.svg +25 -0
  38. package/dist/inspector/canvas/canvasFrame.d.ts +16 -0
  39. package/dist/inspector/canvas/canvasFrame.js +10 -0
  40. package/dist/inspector/canvas/canvasGestures.d.ts +0 -1
  41. package/dist/inspector/canvas/canvasGestures.js +0 -6
  42. package/dist/inspector/canvas/canvasStore.d.ts +6 -0
  43. package/dist/inspector/canvas/canvasStore.js +36 -1
  44. package/dist/inspector/canvas/frameConnection.d.ts +48 -0
  45. package/dist/inspector/canvas/frameConnection.js +73 -0
  46. package/dist/inspector/canvas/frameProtocol.d.ts +18 -1
  47. package/dist/inspector/canvas/frameProtocol.js +36 -0
  48. package/dist/inspector/canvas/frameScrollbarStyles.d.ts +3 -0
  49. package/dist/inspector/canvas/frameScrollbarStyles.js +21 -0
  50. package/dist/inspector/canvas/pageSurface.d.ts +11 -0
  51. package/dist/inspector/canvas/pageSurface.js +34 -0
  52. package/dist/inspector/canvas/projection.d.ts +19 -9
  53. package/dist/inspector/canvas/projection.js +58 -31
  54. package/dist/inspector/canvas/rendererBootstrap.js +4 -1
  55. package/dist/inspector/canvas/rendererElementSelector.js +9 -1
  56. package/dist/inspector/canvas/rendererMessageRouter.d.ts +4 -5
  57. package/dist/inspector/canvas/rendererMessageRouter.js +12 -16
  58. package/dist/inspector/canvas/rendererSelectionProxy.d.ts +7 -1
  59. package/dist/inspector/canvas/rendererSelectionProxy.js +8 -36
  60. package/dist/inspector/canvas/rendererStylesheet.js +15 -0
  61. package/dist/inspector/canvas/sessionStore.d.ts +2 -2
  62. package/dist/inspector/canvas/sessionStore.js +12 -9
  63. package/dist/inspector/canvas/staleChangeDetector.d.ts +1 -1
  64. package/dist/inspector/canvas/staleChangeDetector.js +10 -22
  65. package/dist/inspector/changes/codecs.d.ts +2 -0
  66. package/dist/inspector/changes/codecs.js +27 -3
  67. package/dist/inspector/changes/draftPreview.d.ts +12 -0
  68. package/dist/inspector/changes/draftPreview.js +133 -0
  69. package/dist/inspector/changes/editModel.d.ts +18 -0
  70. package/dist/inspector/changes/editModel.js +16 -3
  71. package/dist/inspector/changes/managedStyleProjection.js +2 -1
  72. package/dist/inspector/changes/previewDiagnostics.js +1 -1
  73. package/dist/inspector/changes/types.d.ts +18 -1
  74. package/dist/inspector/comments/CommentsOverlay.js +18 -16
  75. package/dist/inspector/comments/element.js +10 -3
  76. package/dist/inspector/comments/store.js +3 -2
  77. package/dist/inspector/comments/verification.js +1 -1
  78. package/dist/inspector/componentSemantics/ComponentPropsSection.js +2 -1
  79. package/dist/inspector/componentSemantics/adapterRegistry.d.ts +6 -1
  80. package/dist/inspector/componentSemantics/adapterRegistry.js +23 -1
  81. package/dist/inspector/componentSemantics/changeAction.js +1 -1
  82. package/dist/inspector/componentSemantics/reactRuntime.d.ts +2 -1
  83. package/dist/inspector/componentSemantics/reactRuntime.js +38 -0
  84. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +4 -2
  85. package/dist/inspector/componentSemantics/runtimeBridge.js +17 -0
  86. package/dist/inspector/componentSemantics/textBinding.d.ts +1 -2
  87. package/dist/inspector/componentSemantics/textBinding.js +4 -45
  88. package/dist/inspector/componentSemantics/types.d.ts +11 -0
  89. package/dist/inspector/drafts/draftStore.d.ts +109 -0
  90. package/dist/inspector/drafts/draftStore.js +451 -0
  91. package/dist/inspector/drafts/model.d.ts +9 -1
  92. package/dist/inspector/drafts/model.js +5 -1
  93. package/dist/inspector/drafts/store.d.ts +53 -15
  94. package/dist/inspector/drafts/store.js +143 -169
  95. package/dist/inspector/index.d.ts +2 -4
  96. package/dist/inspector/index.js +2 -264
  97. package/dist/inspector/inline-text/inlineTextEditor.d.ts +1 -1
  98. package/dist/inspector/inline-text/inlineTextEditor.js +1 -2
  99. package/dist/inspector/inline-text/textEditChange.d.ts +1 -1
  100. package/dist/inspector/inspection/bridge.d.ts +1 -1
  101. package/dist/inspector/inspection/bridge.js +2 -1
  102. package/dist/inspector/inspection/browserCssInspection.d.ts +8 -0
  103. package/dist/inspector/inspection/browserCssInspection.js +28 -6
  104. package/dist/inspector/inspection/inspectionSnapshot.d.ts +6 -0
  105. package/dist/inspector/inspection/inspectionSnapshot.js +10 -0
  106. package/dist/inspector/inspection/performanceDiagnostics.d.ts +18 -0
  107. package/dist/inspector/inspection/performanceDiagnostics.js +31 -0
  108. package/dist/inspector/inspection/useBrowserCssInspection.js +17 -38
  109. package/dist/inspector/iterations/capture.d.ts +2 -1
  110. package/dist/inspector/iterations/capture.js +2 -1
  111. package/dist/inspector/iterations/client.d.ts +2 -1
  112. package/dist/inspector/iterations/coordinator.d.ts +4 -3
  113. package/dist/inspector/iterations/coordinator.js +5 -3
  114. package/dist/inspector/iterations/prompt.js +9 -8
  115. package/dist/inspector/overlay/interactionStyles.js +17 -0
  116. package/dist/inspector/pageInspector.d.ts +42 -0
  117. package/dist/inspector/pageInspector.js +342 -0
  118. package/dist/inspector/projection/draftProjection.d.ts +10 -2
  119. package/dist/inspector/projection/draftProjection.js +9 -1
  120. package/dist/inspector/projection/managedStylesheet.js +24 -4
  121. package/dist/inspector/projection/renderedInstance.d.ts +15 -2
  122. package/dist/inspector/projection/renderedInstance.js +60 -7
  123. package/dist/inspector/projection/structuralProjection.d.ts +0 -9
  124. package/dist/inspector/projection/structuralProjection.js +16 -46
  125. package/dist/inspector/projection/textProjection.d.ts +8 -2
  126. package/dist/inspector/projection/textProjection.js +77 -24
  127. package/dist/inspector/prompt/clipboardHandoff.d.ts +14 -0
  128. package/dist/inspector/prompt/clipboardHandoff.js +47 -16
  129. package/dist/inspector/prompt/generatePrompt.d.ts +1 -1
  130. package/dist/inspector/prompt/generatePrompt.js +248 -148
  131. package/dist/inspector/prompt/promptDocument.d.ts +3 -0
  132. package/dist/inspector/prompt/promptDocument.js +8 -0
  133. package/dist/inspector/prompt/promptSettings.js +7 -5
  134. package/dist/inspector/runtime/inspectorStorage.d.ts +17 -0
  135. package/dist/inspector/runtime/inspectorStorage.js +42 -0
  136. package/dist/inspector/runtime/ownedStylesheets.d.ts +20 -0
  137. package/dist/inspector/runtime/ownedStylesheets.js +72 -0
  138. package/dist/inspector/runtime/runtimeConfig.d.ts +4 -0
  139. package/dist/inspector/runtime/runtimeConfig.js +10 -1
  140. package/dist/inspector/runtime/runtimeIdentityClaims.d.ts +29 -0
  141. package/dist/inspector/runtime/runtimeIdentityClaims.js +188 -0
  142. package/dist/inspector/runtime/staticHtmlRuntimeIdentity.d.ts +15 -0
  143. package/dist/inspector/runtime/staticHtmlRuntimeIdentity.js +113 -29
  144. package/dist/inspector/selection/editScope.d.ts +1 -0
  145. package/dist/inspector/selection/editScope.js +13 -2
  146. package/dist/inspector/selection/sourceSite.d.ts +12 -0
  147. package/dist/inspector/selection/sourceSite.js +28 -0
  148. package/dist/inspector/shell/AgentStatusButton.js +2 -2
  149. package/dist/inspector/shell/AppShell.js +6 -2
  150. package/dist/inspector/shell/ChangesLog.js +27 -9
  151. package/dist/inspector/shell/CopyPromptButton.js +74 -26
  152. package/dist/inspector/shell/EmptyState.js +3 -3
  153. package/dist/inspector/shell/InspectorShell.d.ts +0 -3
  154. package/dist/inspector/shell/InspectorShell.js +21 -13
  155. package/dist/inspector/shell/PeekOriginalButton.js +3 -3
  156. package/dist/inspector/shell/interactionStatePreview.d.ts +9 -0
  157. package/dist/inspector/shell/interactionStatePreview.js +112 -0
  158. package/dist/inspector/shell/originalPreview.d.ts +3 -2
  159. package/dist/inspector/shell/originalPreview.js +10 -37
  160. package/dist/inspector/shell/stateValue.js +2 -3
  161. package/dist/inspector/sketch/SketchEditor.js +5 -4
  162. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +2 -1
  163. package/dist/inspector/sketch/SketchFrameOverlay.js +5 -4
  164. package/dist/inspector/sketch/SketchOverlay.d.ts +3 -5
  165. package/dist/inspector/sketch/SketchOverlay.js +139 -263
  166. package/dist/inspector/sketch/SketchPromptPanel.d.ts +1 -1
  167. package/dist/inspector/sketch/SketchPromptPanel.js +8 -1
  168. package/dist/inspector/sketch/SketchThumbnail.d.ts +5 -0
  169. package/dist/inspector/sketch/SketchThumbnail.js +18 -0
  170. package/dist/inspector/sketch/SketchWorkspace.d.ts +1 -2
  171. package/dist/inspector/sketch/SketchWorkspace.js +69 -34
  172. package/dist/inspector/sketch/capture.js +19 -3
  173. package/dist/inspector/sketch/copySketch.d.ts +10 -0
  174. package/dist/inspector/sketch/copySketch.js +52 -0
  175. package/dist/inspector/sketch/drawingTools.d.ts +17 -0
  176. package/dist/inspector/sketch/drawingTools.js +47 -0
  177. package/dist/inspector/sketch/frameHeading.d.ts +9 -0
  178. package/dist/inspector/sketch/frameHeading.js +30 -0
  179. package/dist/inspector/sketch/freehand.js +3 -3
  180. package/dist/inspector/sketch/geometry.d.ts +6 -0
  181. package/dist/inspector/sketch/geometry.js +19 -0
  182. package/dist/inspector/sketch/history.d.ts +30 -0
  183. package/dist/inspector/sketch/history.js +58 -0
  184. package/dist/inspector/sketch/interaction.d.ts +1 -3
  185. package/dist/inspector/sketch/interaction.js +6 -9
  186. package/dist/inspector/sketch/model.d.ts +35 -1
  187. package/dist/inspector/sketch/model.js +116 -61
  188. package/dist/inspector/sketch/persistence.d.ts +14 -0
  189. package/dist/inspector/sketch/persistence.js +63 -7
  190. package/dist/inspector/sketch/raster.d.ts +6 -2
  191. package/dist/inspector/sketch/raster.js +43 -17
  192. package/dist/inspector/sketch/store.d.ts +1 -1
  193. package/dist/inspector/sketch/store.js +1 -1
  194. package/dist/inspector/sketch/videoFrames.d.ts +6 -0
  195. package/dist/inspector/sketch/videoFrames.js +81 -0
  196. package/dist/inspector/styleEditors/BackgroundEditor.js +3 -2
  197. package/dist/inspector/styleEditors/BorderEditor.js +30 -130
  198. package/dist/inspector/styleEditors/BorderRadiusEditor.js +8 -3
  199. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +1 -1
  200. package/dist/inspector/styleEditors/LayoutSection.js +2 -2
  201. package/dist/inspector/styleEditors/borderModel.d.ts +42 -0
  202. package/dist/inspector/styleEditors/borderModel.js +114 -0
  203. package/dist/inspector/styleEditors/layoutValue.js +2 -3
  204. package/dist/inspector/tokens/TokenField.js +40 -7
  205. package/dist/inspector/tokens/TokensPanel.js +2 -1
  206. package/dist/inspector/tokens/editActions.d.ts +3 -3
  207. package/dist/inspector/tokens/editActions.js +15 -3
  208. package/dist/inspector/tokens/resolution/containerQueryEvaluator.d.ts +15 -0
  209. package/dist/inspector/tokens/resolution/containerQueryEvaluator.js +86 -0
  210. package/dist/inspector/tokens/resolution/cssomCollector.js +44 -9
  211. package/dist/inspector/tokens/resolution/nestedSelectors.d.ts +6 -0
  212. package/dist/inspector/tokens/resolution/nestedSelectors.js +67 -0
  213. package/dist/inspector/tokens/resolution/ruleMatching.js +23 -4
  214. package/dist/inspector/tokens/resolution.d.ts +6 -3
  215. package/dist/inspector/tokens/resolution.js +91 -112
  216. package/dist/inspector/tokens/tokenEdits.d.ts +1 -1
  217. package/dist/inspector/tokens/tokenEdits.js +1 -1
  218. package/dist/inspector/ui/InlineStyleWarning.js +2 -2
  219. package/dist/inspector/ui/InspectorStyle.d.ts +8 -0
  220. package/dist/inspector/ui/InspectorStyle.js +26 -0
  221. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  222. package/dist/inspector/ui/PopoverListbox.js +3 -3
  223. package/dist/inspector/ui/Toast.d.ts +14 -0
  224. package/dist/inspector/ui/Toast.js +30 -0
  225. package/dist/inspector/ui/assets/canvasIllustration.d.ts +2 -0
  226. package/dist/inspector/ui/assets/canvasIllustration.js +21 -0
  227. package/dist/inspector/ui/assets/gridIllustration.d.ts +2 -0
  228. package/dist/inspector/ui/assets/{grid.svg → gridIllustration.js} +3 -2
  229. package/dist/inspector/ui/customCursors.d.ts +5 -0
  230. package/dist/inspector/ui/customCursors.js +32 -6
  231. package/dist/inspector/ui/styles.js +12 -10
  232. package/dist/inspector/workspace/commands.js +1 -2
  233. package/dist/inspector/workspace/controller.js +1 -2
  234. package/dist/inspector/workspace/handoff.js +3 -2
  235. package/dist/inspector/workspace/sentItems.d.ts +16 -0
  236. package/dist/inspector/workspace/sentItems.js +33 -0
  237. package/dist/inspector/workspace/timeline.d.ts +24 -10
  238. package/dist/inspector/workspace/timeline.js +118 -96
  239. package/dist/internal/agent-protocol.d.ts +50 -13
  240. package/dist/internal/agent-protocol.js +145 -19
  241. package/dist/nudge-ui.mjs +81 -65
  242. package/dist/project/mcpPackage.d.ts +8 -0
  243. package/dist/project/mcpPackage.js +60 -0
  244. package/package.json +13 -3
  245. package/dist/inspector/canvas/measurementProjection.d.ts +0 -8
  246. package/dist/inspector/canvas/measurementProjection.js +0 -16
  247. package/dist/inspector/changes/changesLog.d.ts +0 -47
  248. package/dist/inspector/changes/changesLog.js +0 -247
  249. package/dist/inspector/changes/draftChanges.d.ts +0 -56
  250. package/dist/inspector/changes/draftChanges.js +0 -300
  251. package/dist/inspector/conformance/projection.d.ts +0 -2
  252. package/dist/inspector/conformance/projection.js +0 -3
  253. package/dist/inspector/inline-text/textChangeBoundary.d.ts +0 -6
  254. package/dist/inspector/inline-text/textChangeBoundary.js +0 -1
  255. package/dist/inspector/overlay/InspectorOverlay.d.ts +0 -5
  256. package/dist/inspector/overlay/InspectorOverlay.js +0 -332
  257. package/dist/inspector/overlay/elementSelector.d.ts +0 -1
  258. package/dist/inspector/overlay/elementSelector.js +0 -111
  259. package/dist/inspector/sketch/SketchChanges.d.ts +0 -2
  260. package/dist/inspector/sketch/SketchChanges.js +0 -72
  261. package/dist/inspector/sketch/SketchEntryButton.d.ts +0 -4
  262. package/dist/inspector/sketch/SketchEntryButton.js +0 -11
  263. package/dist/inspector/sketch/SketchLayersPanel.d.ts +0 -2
  264. package/dist/inspector/sketch/SketchLayersPanel.js +0 -71
@@ -1,11 +1,12 @@
1
- import { SKETCH_ANNOTATION_RADIUS, SKETCH_LIMITS, SKETCH_STROKE_COLOR, } from "./model.js";
1
+ import { SKETCH_ANNOTATION_RADIUS, SKETCH_LIMITS, SKETCH_ACCENT_COLOR, SKETCH_RECTANGLE_FILL_OPACITY, SKETCH_STROKE_COLOR, } from "./model.js";
2
2
  import { sketchStrokeOutline } from "./freehand.js";
3
3
  const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
4
4
  const SKETCH_NOTE_PADDING = 24;
5
- const SKETCH_NOTE_TITLE_HEIGHT = 34;
5
+ const SKETCH_NOTE_TITLE_LINE_HEIGHT = 34;
6
6
  const SKETCH_NOTE_LINE_HEIGHT = 32;
7
7
  const SKETCH_NOTE_FONT = "24px system-ui, sans-serif";
8
8
  const SKETCH_NOTE_TITLE_FONT = "600 24px system-ui, sans-serif";
9
+ const SKETCH_NOTE_FALLBACK_HEADING = "Sketch notes";
9
10
  export function readPngDimensions(bytes) {
10
11
  if (bytes.length < 24)
11
12
  return null;
@@ -63,9 +64,15 @@ export function drawSketchStroke(ctx, stroke, scaleX = 1, scaleY = scaleX) {
63
64
  if (stroke.kind === "rectangle") {
64
65
  ctx.lineCap = "round";
65
66
  ctx.lineJoin = "round";
66
- ctx.strokeStyle = stroke.color || SKETCH_STROKE_COLOR;
67
+ const color = stroke.color || SKETCH_STROKE_COLOR;
68
+ ctx.strokeStyle = color;
69
+ ctx.fillStyle = color;
67
70
  ctx.lineWidth = stroke.width * Math.max(scaleX, scaleY);
68
71
  drawPointPath(ctx, stroke.points, scaleX, scaleY);
72
+ ctx.save();
73
+ ctx.globalAlpha = SKETCH_RECTANGLE_FILL_OPACITY;
74
+ ctx.fill();
75
+ ctx.restore();
69
76
  ctx.stroke();
70
77
  }
71
78
  else {
@@ -93,7 +100,7 @@ export function drawSketchAnnotation(ctx, annotation, scaleX = 1, scaleY = scale
93
100
  const y = annotation.point.y * scaleY;
94
101
  const radius = SKETCH_ANNOTATION_RADIUS * scale;
95
102
  ctx.save();
96
- ctx.fillStyle = SKETCH_STROKE_COLOR;
103
+ ctx.fillStyle = SKETCH_ACCENT_COLOR;
97
104
  ctx.beginPath();
98
105
  ctx.arc(x, y, radius, 0, Math.PI * 2);
99
106
  ctx.fill();
@@ -159,6 +166,14 @@ function wrapText(ctx, text, maxWidth) {
159
166
  lines.push(line);
160
167
  return lines;
161
168
  }
169
+ /** Wraps the frame heading that names the sketch target to the panel width. */
170
+ function wrapSketchHeading(ctx, width, heading) {
171
+ const text = heading.trim();
172
+ if (!text)
173
+ return [];
174
+ ctx.font = SKETCH_NOTE_TITLE_FONT;
175
+ return wrapText(ctx, text, Math.max(1, width - SKETCH_NOTE_PADDING * 2));
176
+ }
162
177
  /** Wraps the description and numbered annotation notes to the panel width. */
163
178
  function wrapSketchNotes(ctx, width, description, annotations) {
164
179
  const notes = [
@@ -172,29 +187,34 @@ function wrapSketchNotes(ctx, width, description, annotations) {
172
187
  const textWidth = Math.max(1, width - SKETCH_NOTE_PADDING * 2);
173
188
  return notes.flatMap((note) => wrapText(ctx, note, textWidth));
174
189
  }
175
- function sketchNotePanelHeight(lineCount) {
176
- if (lineCount === 0)
190
+ function sketchNotePanelHeight(headingLineCount, noteLineCount) {
191
+ if (headingLineCount + noteLineCount === 0)
177
192
  return 0;
178
- return SKETCH_NOTE_PADDING * 2 + SKETCH_NOTE_TITLE_HEIGHT + lineCount * SKETCH_NOTE_LINE_HEIGHT;
193
+ return SKETCH_NOTE_PADDING * 2
194
+ + headingLineCount * SKETCH_NOTE_TITLE_LINE_HEIGHT
195
+ + noteLineCount * SKETCH_NOTE_LINE_HEIGHT;
179
196
  }
180
- function drawSketchNotes(ctx, width, top, lines) {
197
+ function drawSketchNotes(ctx, width, top, headingLines, noteLines) {
181
198
  ctx.save();
182
- ctx.fillStyle = SKETCH_STROKE_COLOR;
183
- ctx.fillRect(0, top, width, sketchNotePanelHeight(lines.length));
199
+ ctx.fillStyle = SKETCH_ACCENT_COLOR;
200
+ ctx.fillRect(0, top, width, sketchNotePanelHeight(headingLines.length, noteLines.length));
184
201
  ctx.fillStyle = "#ffffff";
185
202
  ctx.textAlign = "left";
186
203
  ctx.textBaseline = "top";
187
204
  ctx.font = SKETCH_NOTE_TITLE_FONT;
188
- ctx.fillText("Sketch notes", SKETCH_NOTE_PADDING, top + SKETCH_NOTE_PADDING);
205
+ let y = top + SKETCH_NOTE_PADDING;
206
+ for (const line of headingLines) {
207
+ ctx.fillText(line, SKETCH_NOTE_PADDING, y);
208
+ y += SKETCH_NOTE_TITLE_LINE_HEIGHT;
209
+ }
189
210
  ctx.font = SKETCH_NOTE_FONT;
190
- let y = top + SKETCH_NOTE_PADDING + SKETCH_NOTE_TITLE_HEIGHT;
191
- for (const line of lines) {
211
+ for (const line of noteLines) {
192
212
  ctx.fillText(line, SKETCH_NOTE_PADDING, y);
193
213
  y += SKETCH_NOTE_LINE_HEIGHT;
194
214
  }
195
215
  ctx.restore();
196
216
  }
197
- export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight, strokes, annotations = [], description = "") {
217
+ export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight, strokes, annotations = [], description = "", heading = SKETCH_NOTE_FALLBACK_HEADING) {
198
218
  await validatePngBlob(originalImage, { width: imageWidth, height: imageHeight });
199
219
  if (typeof document === "undefined")
200
220
  throw new Error("Sketch rendering requires a browser document.");
@@ -205,8 +225,9 @@ export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight,
205
225
  const context = canvas.getContext("2d");
206
226
  if (!context)
207
227
  throw new Error("The browser could not create a sketch canvas.");
228
+ const headingLines = wrapSketchHeading(context, imageWidth, heading);
208
229
  const noteLines = wrapSketchNotes(context, imageWidth, description, annotations);
209
- const outputHeight = imageHeight + sketchNotePanelHeight(noteLines.length);
230
+ const outputHeight = imageHeight + sketchNotePanelHeight(headingLines.length, noteLines.length);
210
231
  if (outputHeight > SKETCH_LIMITS.imageEdge) {
211
232
  throw new Error("Sketch notes are too long to fit below the captured image.");
212
233
  }
@@ -215,8 +236,9 @@ export async function renderAnnotatedPng(originalImage, imageWidth, imageHeight,
215
236
  context.drawImage(image.source, 0, 0, imageWidth, imageHeight);
216
237
  drawSketchStrokes(context, strokes);
217
238
  drawSketchAnnotations(context, annotations);
218
- if (noteLines.length > 0)
219
- drawSketchNotes(context, imageWidth, imageHeight, noteLines);
239
+ if (headingLines.length > 0 || noteLines.length > 0) {
240
+ drawSketchNotes(context, imageWidth, imageHeight, headingLines, noteLines);
241
+ }
220
242
  const result = await canvasPng(canvas);
221
243
  if (result.size > SKETCH_LIMITS.imageBytes) {
222
244
  throw new Error("This sketch is too detailed to save within the 2 MiB image limit.");
@@ -238,6 +260,10 @@ export async function blobToBase64(blob) {
238
260
  throw new Error("This browser cannot prepare sketch attachments.");
239
261
  return btoa(binary);
240
262
  }
263
+ /**
264
+ * Accepts a pending blob so callers can start the write inside the user gesture
265
+ * (required by Safari) and resolve the image once rendering finishes.
266
+ */
241
267
  export async function copyImageToClipboard(blob) {
242
268
  if (typeof navigator === "undefined" || !navigator.clipboard?.write || typeof ClipboardItem === "undefined") {
243
269
  throw new Error("Image clipboard access is unavailable. Use Download image instead.");
@@ -35,7 +35,7 @@ export declare function markSketchesHandingOff(entries: readonly {
35
35
  readonly id: string;
36
36
  readonly revision: number;
37
37
  }[], batchRevision: number, localBatchId: string, agentRequestId?: string): Promise<boolean>;
38
- export declare function settleSketchDispatch(batchRevision: number | undefined, state: "working" | "completed" | "failed" | "interrupted", agentRequestId?: string, localBatchId?: string): Promise<boolean>;
38
+ export declare function settleSketchDispatch(batchRevision: number | undefined, state: "delivered" | "working" | "completed" | "failed" | "interrupted", agentRequestId?: string, localBatchId?: string): Promise<boolean>;
39
39
  export declare function markSketchesHandedOff(entries: readonly {
40
40
  readonly id: string;
41
41
  readonly revision: number;
@@ -325,7 +325,7 @@ export function settleSketchDispatch(batchRevision, state, agentRequestId, local
325
325
  }
326
326
  return true;
327
327
  }
328
- const nextHandoffs = changes.map((handoff) => updateHandoff(handoff, state === "working" ? {
328
+ const nextHandoffs = changes.map((handoff) => updateHandoff(handoff, (state === "delivered" || state === "working") ? {
329
329
  state: "handing-off",
330
330
  transport: "direct",
331
331
  ...(agentRequestId ? { agentRequestId } : {}),
@@ -0,0 +1,6 @@
1
+ /** Supplies still images to the DOM renderer while live videos stay mounted. */
2
+ export declare function prepareSketchVideoFrames(targetDocument: Document): {
3
+ readonly filter: (node: Node) => boolean;
4
+ readonly prepareClone: (node: Node) => void;
5
+ readonly restore: () => void;
6
+ };
@@ -0,0 +1,81 @@
1
+ import { SKETCH_LIMITS } from "./model.js";
2
+ const FRAME_ATTRIBUTE = "data-nudge-ui-video-frame";
3
+ const EMPTY_FRAME = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
4
+ function* videoElements(root) {
5
+ yield* root.querySelectorAll("video");
6
+ for (const element of root.querySelectorAll("*")) {
7
+ if (element.shadowRoot)
8
+ yield* videoElements(element.shadowRoot);
9
+ }
10
+ }
11
+ function videoFrame(video) {
12
+ const image = video.ownerDocument.createElement("img");
13
+ image.src = video.poster || EMPTY_FRAME;
14
+ image.width = video.videoWidth || video.clientWidth || 300;
15
+ image.height = video.videoHeight || video.clientHeight || 150;
16
+ const style = video.ownerDocument.defaultView?.getComputedStyle(video);
17
+ if (style) {
18
+ for (const property of style) {
19
+ image.style.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property));
20
+ }
21
+ }
22
+ // Preserve the replaced element's layout even when its CSS size is auto.
23
+ image.style.width = style?.width && style.width !== "auto" ? style.width : `${video.clientWidth}px`;
24
+ image.style.height = style?.height && style.height !== "auto" ? style.height : `${video.clientHeight}px`;
25
+ if (video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0) {
26
+ const canvas = video.ownerDocument.createElement("canvas");
27
+ const scale = Math.min(1, SKETCH_LIMITS.imageEdge / Math.max(video.videoWidth, video.videoHeight));
28
+ canvas.width = Math.max(1, Math.round(video.videoWidth * scale));
29
+ canvas.height = Math.max(1, Math.round(video.videoHeight * scale));
30
+ try {
31
+ const context = canvas.getContext("2d");
32
+ if (context) {
33
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
34
+ image.src = canvas.toDataURL("image/png");
35
+ }
36
+ }
37
+ catch {
38
+ // Cross-origin frames can taint a canvas. Keep the poster or empty frame.
39
+ }
40
+ }
41
+ return image;
42
+ }
43
+ /** Supplies still images to the DOM renderer while live videos stay mounted. */
44
+ export function prepareSketchVideoFrames(targetDocument) {
45
+ const frames = new Map();
46
+ const placeholders = [];
47
+ for (const video of videoElements(targetDocument)) {
48
+ const id = String(frames.size);
49
+ frames.set(id, videoFrame(video));
50
+ const placeholder = targetDocument.createElement("img");
51
+ placeholder.hidden = true;
52
+ placeholder.src = EMPTY_FRAME;
53
+ placeholder.setAttribute(FRAME_ATTRIBUTE, id);
54
+ // The hidden sibling preserves capture order without detaching or seeking
55
+ // the application's video. Only its clone receives the video's styles.
56
+ video.before(placeholder);
57
+ placeholders.push(placeholder);
58
+ }
59
+ return {
60
+ filter: (node) => node.nodeType !== 1 || node.localName !== "video",
61
+ prepareClone: (node) => {
62
+ if (node.nodeType !== 1)
63
+ return;
64
+ const element = node;
65
+ const id = element.getAttribute(FRAME_ATTRIBUTE);
66
+ const frame = id === null ? undefined : frames.get(id);
67
+ if (!frame)
68
+ return;
69
+ element.removeAttribute(FRAME_ATTRIBUTE);
70
+ element.removeAttribute("hidden");
71
+ element.style.cssText = frame.style.cssText;
72
+ element.setAttribute("src", frame.src);
73
+ element.setAttribute("width", String(frame.width));
74
+ element.setAttribute("height", String(frame.height));
75
+ },
76
+ restore: () => {
77
+ for (const placeholder of placeholders)
78
+ placeholder.remove();
79
+ },
80
+ };
81
+ }
@@ -6,7 +6,8 @@ import { interpretTokenValue } from "../../css/value-semantics/tokenInterpretati
6
6
  import { getNudgeUiTokenEntries } from "../runtime/runtimeConfig.js";
7
7
  import { colorValueToHex, TokenValueField } from "../tokens/TokenField.js";
8
8
  import { setStyles } from "../tokens/editActions.js";
9
- import { createChangeHistoryGroup, getActiveDraftChanges } from "../changes/draftChanges.js";
9
+ import { getActiveDraftChanges } from "../drafts/store.js";
10
+ import { createChangeHistoryGroup } from "../workspace/timeline.js";
10
11
  import { getStateStyleValue } from "../shell/stateValue.js";
11
12
  import { ControlSurface } from "../ui/ControlSurface.js";
12
13
  import { IconButton } from "../ui/IconButton.js";
@@ -26,7 +27,7 @@ export function BackgroundEditor(props) {
26
27
  const elements = selection?.domElements ?? [el];
27
28
  const source = readBackgroundLayers((property, fallback) => getStateStyleValue(el, property, fallback));
28
29
  const base = source.at(-1);
29
- const authoredColor = props.tokenRow?.property === "background-color"
30
+ const authoredColor = props.tokenRow?.property === "background-color" || props.tokenRow?.property === "background"
30
31
  ? props.tokenRow.authored ?? props.tokenRow.declaredValue : undefined;
31
32
  if (base?.baseColor && authoredColor) {
32
33
  const table = Object.fromEntries((props.entries ?? getNudgeUiTokenEntries()).map((entry) => [entry.name, entry]));
@@ -3,9 +3,8 @@ import { useEffect, useState } from "react";
3
3
  import { IconBorderSides, IconBorderStyle2, IconCheck, IconMinus, IconPlus, } from "@tabler/icons-react";
4
4
  import { findTokenRow } from "./rowLookup.js";
5
5
  import { TokenField } from "../tokens/TokenField.js";
6
- import { setStyle, setStyles } from "../tokens/editActions.js";
6
+ import { setStyles } from "../tokens/editActions.js";
7
7
  import { FieldRow } from "../ui/FieldRow.js";
8
- import { SIDE_NAMES } from "../ui/SideValuesField.js";
9
8
  import { IconButton } from "../ui/IconButton.js";
10
9
  import { ToggleButton } from "../ui/ToggleButton.js";
11
10
  import { formatInspectorLabel } from "../ui/labels.js";
@@ -13,101 +12,22 @@ import { getStateStyleValue } from "../shell/stateValue.js";
13
12
  import { PopoverListbox } from "../ui/PopoverListbox.js";
14
13
  import { ControlSurface } from "../ui/ControlSurface.js";
15
14
  import { getNudgeUiTokenEntries } from "../runtime/runtimeConfig.js";
16
- import { isZeroCssValue } from "./stylePresence.js";
17
15
  import { useFieldVisibility } from "./useFieldVisibility.js";
18
16
  import { Tooltip } from "../ui/Tooltip.js";
19
17
  import { inlineStyleWarningContent } from "../ui/InlineStyleWarning.js";
20
18
  import { inlineBlockedBy } from "./inlineAuthored.js";
21
- const BORDER_STYLES = ["none", "hidden", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"];
22
- const INVISIBLE_BORDER_STYLES = new Set(["none", "hidden"]);
23
- const BORDER_SIDES = SIDE_NAMES.map((side) => `border-${side}`);
24
- function borderStyleValue(el, rows, property) {
25
- const row = findTokenRow(rows, property);
26
- const structured = row?.structure?.style?.trim();
27
- if (structured)
28
- return structured.toLowerCase();
29
- const authored = (row?.authored ?? row?.declaredValue ?? "").trim().toLowerCase();
30
- if (authored && BORDER_STYLES.includes(authored))
31
- return authored;
32
- return (getStateStyleValue(el, property, "none") || "none").toLowerCase();
19
+ import { BORDER_SIDES, BORDER_STYLES, borderDeclarations, borderProperty, hasVisibleBorder, readBorderEditor, } from "./borderModel.js";
20
+ function stateStyleReader(el) {
21
+ return (property, fallback) => getStateStyleValue(el, property, fallback);
33
22
  }
34
- function borderWidthValue(el, rows, property) {
35
- const row = findTokenRow(rows, property);
36
- if (row?.structure?.width)
37
- return row.structure.width.trim();
38
- return (row?.authored
39
- || row?.declaredValue
40
- || row?.resolvedValue
41
- || getStateStyleValue(el, property, "0px")
42
- || "0px").trim();
43
- }
44
- function borderColorValue(el, rows, property) {
45
- const row = findTokenRow(rows, property);
46
- if (row?.structure?.color)
47
- return row.structure.color.trim();
48
- return (row?.authored
49
- || row?.declaredValue
50
- || row?.resolvedValue
51
- || getStateStyleValue(el, property, "currentcolor")
52
- || "currentcolor").trim();
53
- }
54
- /** Effective per-side value used to decide linked vs individual UI. */
55
- function sideComponentSignature(el, rows, property) {
56
- const row = findTokenRow(rows, property);
57
- if (property.endsWith("-width")) {
58
- return `w:${borderWidthValue(el, rows, property).toLowerCase()}|t:${row?.tokenName ?? ""}`;
59
- }
60
- if (property.endsWith("-style")) {
61
- return `s:${borderStyleValue(el, rows, property)}`;
62
- }
63
- if (property.endsWith("-color")) {
64
- return `c:${borderColorValue(el, rows, property).toLowerCase()}|t:${row?.tokenName ?? ""}`;
65
- }
66
- return `${row?.authored ?? ""}|${row?.resolvedValue ?? getStateStyleValue(el, property)}`;
67
- }
68
- function sideValueForLink(el, rows, property, fallback = "") {
69
- if (property.endsWith("-width"))
70
- return borderWidthValue(el, rows, property) || fallback;
71
- if (property.endsWith("-style"))
72
- return borderStyleValue(el, rows, property) || fallback;
73
- if (property.endsWith("-color"))
74
- return borderColorValue(el, rows, property) || fallback;
75
- return fallback;
76
- }
77
- function borderLinkDeclaration(el, rows, shorthand, sideProperties) {
78
- const sharedValue = sideValueForLink(el, rows, sideProperties[0], shorthand === "border-style" ? "solid" : "");
79
- return sharedValue ? { property: shorthand, value: sharedValue } : null;
80
- }
81
- function linkedTokenRow(rows, shorthand, sideProperties) {
82
- const direct = findTokenRow(rows, shorthand);
83
- if (direct)
84
- return direct;
85
- const first = findTokenRow(rows, sideProperties[0]);
86
- return first ? { ...first, property: shorthand } : null;
87
- }
88
- /**
89
- * Sides are linked when every face shares the same effective component value.
90
- * Uses structured width/style/color parts (not the full authored shorthand string)
91
- * so four different `border-*` shorthands correctly expand the UI.
92
- */
93
- function valuesAreLinked(el, rows, shorthand, sideProperties) {
94
- const hasDirectRow = Boolean(findTokenRow(rows, shorthand));
95
- const hasSideRows = sideProperties.some((property) => Boolean(findTokenRow(rows, property)));
96
- // A lone shorthand with no per-side overrides is inherently linked.
97
- if (hasDirectRow && !hasSideRows)
98
- return true;
99
- const signatures = sideProperties.map((property) => sideComponentSignature(el, rows, property));
100
- return new Set(signatures).size === 1;
101
- }
102
- /** Initial visibility follows the effective border, including stylesheet resets. */
103
- function hasBorderPresence(el) {
104
- return SIDE_NAMES.some((side) => {
105
- const style = getStateStyleValue(el, `border-${side}-style`, "none").toLowerCase();
106
- if (INVISIBLE_BORDER_STYLES.has(style))
107
- return false;
108
- const width = getStateStyleValue(el, `border-${side}-width`, "0px");
109
- return !isZeroCssValue(width);
110
- });
23
+ /** Reports whether the intent produced any declarations to write. */
24
+ function commitBorderIntent(target, source, intent, onAfterEdit) {
25
+ const declarations = borderDeclarations(source, intent);
26
+ if (declarations.length === 0)
27
+ return false;
28
+ setStyles(target, declarations);
29
+ onAfterEdit?.();
30
+ return true;
111
31
  }
112
32
  /**
113
33
  * Linked presentation from cascade data + optional user toggle.
@@ -147,36 +67,25 @@ export function BorderEditor(props) {
147
67
  const editTarget = selection?.target ?? el;
148
68
  const isGroup = Boolean(selection && selection.elements.length > 1);
149
69
  const allEntries = entries ?? getNudgeUiTokenEntries();
150
- const borderWidthProperties = BORDER_SIDES.map((side) => `${side}-width`);
151
- const borderStyleProperties = BORDER_SIDES.map((side) => `${side}-style`);
152
- const borderColorProperties = BORDER_SIDES.map((side) => `${side}-color`);
153
70
  const borderBlockedBy = inlineBlockedBy(selectedElements, "border", "border-width", "border-style", "border-color");
154
- const borderWidthDataLinked = valuesAreLinked(el, tokenRows, "border-width", borderWidthProperties);
155
- const borderStyleDataLinked = valuesAreLinked(el, tokenRows, "border-style", borderStyleProperties);
156
- const borderColorDataLinked = valuesAreLinked(el, tokenRows, "border-color", borderColorProperties);
157
- const allDataLinked = !isGroup && borderWidthDataLinked && borderStyleDataLinked && borderColorDataLinked;
158
- const [borderLinked, setBorderLinked] = useBorderLinkedState(allDataLinked, el);
159
- const borderRow = findTokenRow(tokenRows, "border");
160
- const hasStructuredBorderRows = tokenRows.some((row) => Boolean(row.structure));
161
- const rawBorderFallback = !isGroup && Boolean(borderRow && !borderRow.structure && !hasStructuredBorderRows);
162
- const linkedBorderStyle = borderStyleValue(el, tokenRows, "border-style");
163
- const showWidthAndColor = !(borderLinked && INVISIBLE_BORDER_STYLES.has(linkedBorderStyle));
164
- const hasBorder = selectedElements.some(hasBorderPresence);
71
+ const source = { rows: tokenRows, computed: stateStyleReader(el) };
72
+ const border = readBorderEditor(source, { group: isGroup });
73
+ const [borderLinked, setBorderLinked] = useBorderLinkedState(border.sidesLinked, el);
74
+ const showWidthAndColor = !borderLinked || border.linkedStyleDraws;
75
+ const hasBorder = selectedElements.some((element) => hasVisibleBorder(stateStyleReader(element)));
165
76
  const visibility = useFieldVisibility(selectedElements, "border", hasBorder);
166
77
  const showBorderControls = visibility.visible;
167
78
  function handleAddBorder() {
168
79
  if (borderBlockedBy)
169
80
  return;
170
- setStyle(editTarget, "border", "1px solid");
81
+ commitBorderIntent(editTarget, source, { kind: "add" }, onAfterEdit);
171
82
  visibility.show();
172
- onAfterEdit?.();
173
83
  }
174
84
  function handleRemoveBorder() {
175
85
  if (borderBlockedBy)
176
86
  return;
177
- setStyle(editTarget, "border", "0 solid");
87
+ commitBorderIntent(editTarget, source, { kind: "remove" }, onAfterEdit);
178
88
  visibility.hide();
179
- onAfterEdit?.();
180
89
  }
181
90
  function handleExpand() {
182
91
  setBorderLinked(false);
@@ -185,23 +94,18 @@ export function BorderEditor(props) {
185
94
  if (borderBlockedBy)
186
95
  return;
187
96
  setBorderLinked(true);
188
- const declarations = [
189
- borderLinkDeclaration(el, tokenRows, "border-style", borderStyleProperties),
190
- borderLinkDeclaration(el, tokenRows, "border-width", borderWidthProperties),
191
- borderLinkDeclaration(el, tokenRows, "border-color", borderColorProperties),
192
- ].filter((declaration) => declaration !== null);
193
- if (declarations.length > 0) {
194
- setStyles(editTarget, declarations);
195
- onAfterEdit?.();
196
- }
97
+ commitBorderIntent(editTarget, source, { kind: "link-sides" }, onAfterEdit);
98
+ }
99
+ function handleSelectStyle(property, style) {
100
+ return commitBorderIntent(editTarget, source, { kind: "set-style", property, style }, onAfterEdit);
197
101
  }
198
- const sideRows = SIDE_NAMES.map((side, index) => ({
102
+ const sideRows = BORDER_SIDES.map((side) => ({
199
103
  side,
200
- color: (_jsx(ControlSurface, { children: _jsx(TokenField, { property: borderColorProperties[index], selection: selection, tokenRow: findTokenRow(tokenRows, borderColorProperties[index]), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) })),
201
- width: (_jsx(ControlSurface, { children: _jsx(TokenField, { property: borderWidthProperties[index], selection: selection, tokenRow: findTokenRow(tokenRows, borderWidthProperties[index]), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit, chipVariant: "small" }) })),
202
- style: (_jsx(BorderStyleSettingsMenu, { property: borderStyleProperties[index], tokenRow: findTokenRow(tokenRows, borderStyleProperties[index]), domElement: el, editTarget: editTarget, selection: selection, dataTest: `border-style-${side}`, onAfterEdit: onAfterEdit })),
104
+ color: (_jsx(ControlSurface, { children: _jsx(TokenField, { property: borderProperty("color", side), selection: selection, tokenRow: findTokenRow(tokenRows, borderProperty("color", side)), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) })),
105
+ width: (_jsx(ControlSurface, { children: _jsx(TokenField, { property: borderProperty("width", side), selection: selection, tokenRow: findTokenRow(tokenRows, borderProperty("width", side)), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit, chipVariant: "small" }) })),
106
+ style: (_jsx(BorderStyleSettingsMenu, { property: borderProperty("style", side), tokenRow: findTokenRow(tokenRows, borderProperty("style", side)), domElement: el, selection: selection, dataTest: `border-style-${side}`, onSelectStyle: handleSelectStyle })),
203
107
  }));
204
- return (_jsxs("div", { className: `editor`, "data-test": "border-editor", children: [_jsxs("div", { className: "editor__title-row", children: [_jsx("div", { className: "editor__title", children: "Border" }), showBorderControls ? (_jsx(Tooltip, { content: inlineStyleWarningContent(borderBlockedBy), children: _jsx(IconButton, { variant: "quiet", label: "Remove Border", "data-test": "remove-border", className: "border__remove", disabled: borderBlockedBy !== null, onClick: handleRemoveBorder, children: _jsx(IconMinus, { size: 16, "aria-hidden": "true" }) }) })) : (_jsx(IconButton, { variant: "quiet", label: "Add Border", "data-test": "add-border", className: "border__add", disabled: borderBlockedBy !== null, onClick: handleAddBorder, children: _jsx(IconPlus, { size: 16, "aria-hidden": "true" }) }))] }), showBorderControls && (_jsx("div", { className: "border", "data-expanded": borderLinked ? "false" : "true", children: rawBorderFallback ? (_jsx(FieldRow, { label: "Border", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border", selection: selection, tokenRow: borderRow, domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) }) })) : borderLinked ? (_jsxs("div", { className: "border__linked-row", children: [showWidthAndColor ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "border__linked-control border__linked-control--color", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border-color", selection: selection, tokenRow: linkedTokenRow(tokenRows, "border-color", borderColorProperties), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) }) }), _jsx("div", { className: "border__linked-control border__linked-control--width", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border-width", selection: selection, tokenRow: linkedTokenRow(tokenRows, "border-width", borderWidthProperties), domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit, chipVariant: "small" }) }) })] })) : null, _jsx(BorderStyleSettingsMenu, { property: "border-style", tokenRow: linkedTokenRow(tokenRows, "border-style", borderStyleProperties), domElement: el, editTarget: editTarget, selection: selection, onAfterEdit: onAfterEdit }), _jsx(ToggleButton, { variant: "quiet", size: "default", "data-test": "border-expand", label: "Edit Individual Border Sides", pressed: !borderLinked, onPressedChange: (pressed) => {
108
+ return (_jsxs("div", { className: `editor`, "data-test": "border-editor", children: [_jsxs("div", { className: "editor__title-row", children: [_jsx("div", { className: "editor__title", children: "Border" }), showBorderControls ? (_jsx(Tooltip, { content: inlineStyleWarningContent(borderBlockedBy), children: _jsx(IconButton, { variant: "quiet", label: "Remove Border", "data-test": "remove-border", className: "border__remove", disabled: borderBlockedBy !== null, onClick: handleRemoveBorder, children: _jsx(IconMinus, { size: 16, "aria-hidden": "true" }) }) })) : (_jsx(IconButton, { variant: "quiet", label: "Add Border", "data-test": "add-border", className: "border__add", disabled: borderBlockedBy !== null, onClick: handleAddBorder, children: _jsx(IconPlus, { size: 16, "aria-hidden": "true" }) }))] }), showBorderControls && (_jsx("div", { className: "border", "data-expanded": borderLinked ? "false" : "true", children: border.rawShorthandRow ? (_jsx(FieldRow, { label: "Border", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border", selection: selection, tokenRow: border.rawShorthandRow, domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) }) })) : borderLinked ? (_jsxs("div", { className: "border__linked-row", children: [showWidthAndColor ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "border__linked-control border__linked-control--color", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border-color", selection: selection, tokenRow: border.linkedRows.color, domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit }) }) }), _jsx("div", { className: "border__linked-control border__linked-control--width", children: _jsx(ControlSurface, { children: _jsx(TokenField, { property: "border-width", selection: selection, tokenRow: border.linkedRows.width, domElement: el, editTarget: editTarget, entries: allEntries, onAfterEdit: onAfterEdit, chipVariant: "small" }) }) })] })) : null, _jsx(BorderStyleSettingsMenu, { property: "border-style", tokenRow: border.linkedRows.style, domElement: el, selection: selection, onSelectStyle: handleSelectStyle }), _jsx(ToggleButton, { variant: "quiet", size: "default", "data-test": "border-expand", label: "Edit Individual Border Sides", pressed: !borderLinked, onPressedChange: (pressed) => {
205
109
  if (pressed)
206
110
  handleExpand();
207
111
  }, children: _jsx(IconBorderSides, { size: 16, "aria-hidden": "true" }) })] })) : (_jsxs("div", { className: "border__expanded", children: [_jsxs("div", { className: "border__expanded-header", children: [_jsx("span", { className: "side-values__label", children: formatInspectorLabel("Individual Sides") }), _jsx(ToggleButton, { variant: "quiet", size: "default", "data-test": "border-collapse", label: "Link All Border Sides", pressed: !borderLinked, disabled: borderBlockedBy !== null, onPressedChange: (pressed) => {
@@ -231,7 +135,7 @@ function BorderSideIndicator({ side }) {
231
135
  const { outline, emphasis } = BORDER_SIDE_ICON_PATHS[side];
232
136
  return (_jsxs("svg", { className: "side-values__icon side-values__side-icon", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [_jsx("path", { d: outline, stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)", strokeLinecap: "square", strokeOpacity: "0.2" }), _jsx("line", { ...emphasis, stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)", strokeLinecap: "round" })] }));
233
137
  }
234
- function BorderStyleSettingsMenu({ property, tokenRow, domElement: el, editTarget, selection, dataTest = "border-style-settings", onAfterEdit }) {
138
+ function BorderStyleSettingsMenu({ property, tokenRow, domElement: el, selection, dataTest = "border-style-settings", onSelectStyle }) {
235
139
  const structured = tokenRow?.structure?.style?.trim().toLowerCase() ?? "";
236
140
  const selectedValue = selection?.getProperty(property)?.value;
237
141
  const mixed = selectedValue?.kind === "mixed";
@@ -244,14 +148,10 @@ function BorderStyleSettingsMenu({ property, tokenRow, domElement: el, editTarge
244
148
  setValue(mixed ? "Mixed" : commonValue || structured || getStateStyleValue(el, property, "none") || "none");
245
149
  }, [commonValue, el, mixed, property, structured]);
246
150
  function handleChange(next) {
247
- if (blockedBy)
248
- return;
249
- if (!BORDER_STYLES.includes(next))
151
+ if (blockedBy || !onSelectStyle(property, next))
250
152
  return;
251
153
  setValue(next);
252
- setStyle(editTarget ?? el, property, next);
253
154
  setOpen(false);
254
- onAfterEdit?.();
255
155
  }
256
156
  return (_jsx(PopoverListbox, { query: "", value: null, open: open, triggerElement: (_jsx(IconButton, { variant: "quiet", size: "default", label: `Border style: ${formatInspectorLabel(value)}`, "data-test": dataTest, "data-current-style": value, children: _jsx(IconBorderStyle2, { size: 16, "aria-hidden": "true" }) })), triggerDataTest: dataTest, triggerAriaLabel: "Border style settings", triggerTooltip: inlineStyleWarningContent(blockedBy), disabled: blockedBy !== null, items: BORDER_STYLES.map((style) => ({
257
157
  value: style,
@@ -28,7 +28,13 @@ function cornerAuthoredSignature(rows, corner) {
28
28
  const row = findTokenRow(rows, corner);
29
29
  return `${row?.authored ?? row?.declaredValue ?? ""}|${row?.tokenName ?? ""}`;
30
30
  }
31
- function cornersAreLinked(rows) {
31
+ function cornersAreLinked(rows, selection) {
32
+ if (selection && selection.elements.length > 1) {
33
+ const values = BORDER_RADIUS_CORNERS.map((corner) => selection.getProperty(corner)?.value);
34
+ const first = values[0];
35
+ return first?.kind === "common"
36
+ && values.every((value) => value?.kind === "common" && value.value === first.value);
37
+ }
32
38
  const signatures = BORDER_RADIUS_CORNERS.map((corner) => cornerAuthoredSignature(rows, corner));
33
39
  return new Set(signatures).size === 1;
34
40
  }
@@ -50,9 +56,8 @@ export function BorderRadiusEditor(props) {
50
56
  const { element, selection, entries, tokenRows = [], onAfterEdit, embedded = false } = props;
51
57
  const el = element.domElement;
52
58
  const editTarget = selection?.target ?? el;
53
- const isGroup = Boolean(selection && selection.elements.length > 1);
54
59
  const allEntries = entries ?? getNudgeUiTokenEntries();
55
- const dataLinked = !isGroup && cornersAreLinked(tokenRows);
60
+ const dataLinked = cornersAreLinked(tokenRows, selection);
56
61
  const blockedBy = inlineBlockedBy(selection?.domElements ?? [el], "border-radius");
57
62
  const [userUnlinked, setUserUnlinked] = useState(false);
58
63
  const [userLinked, setUserLinked] = useState(false);
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import { IconPhoto, IconUpload } from "@tabler/icons-react";
4
4
  import { TokenValueField } from "../tokens/TokenField.js";
5
5
  import { ControlSurface } from "../ui/ControlSurface.js";
6
- import { getActiveDraftChanges } from "../changes/draftChanges.js";
6
+ import { getActiveDraftChanges } from "../drafts/store.js";
7
7
  import { getStateStyleValue } from "../shell/stateValue.js";
8
8
  import { Button } from "../ui/Button.js";
9
9
  import { InlineStyleWarning } from "../ui/InlineStyleWarning.js";
@@ -109,7 +109,7 @@ export function LayoutSection(props) {
109
109
  const showPositionInsets = selection
110
110
  ? selection.supportsRole("inset-position")
111
111
  : position === "absolute" || position === "fixed";
112
- return (_jsxs("div", { className: "editor", "data-test": "layout-section", children: [_jsx("div", { className: "editor__title", children: "Layout" }), _jsxs("div", { className: "layout", children: [_jsxs("div", { className: "layout__basic", children: [_jsx(SizeSection, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsxs("div", { className: "layout__tool-row", children: [_jsx(LayoutDropdown, { property: "display", options: DISPLAY_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(LayoutDropdown, { property: "position", options: POSITION_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] }), isFlexContainer ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-container", children: [_jsxs("div", { className: "layout__flex-heading", children: [_jsx("div", { className: "editor__title", children: "Flex" }), _jsx(FlexSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), _jsxs("div", { className: "layout__flex-lower", children: [mixedFlexDirection ? (_jsx("div", { className: "layout__alignment-grid", "data-test": "layout-flex-axis-mixed", children: "Choose a shared flex direction to edit physical alignment." })) : (_jsx(FlexAlignmentGrid, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })), _jsxs("div", { className: "layout__gap-column", "data-test": "layout-gap", children: [_jsxs("div", { className: "layout__direction-tools", children: [_jsx(FlexDirectionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexWrapToggle, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), _jsxs("div", { className: "layout__gap-fields", children: [_jsxs("div", { className: "layout__spacing-primary", children: [_jsx(FlexGapField, { property: relevantGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit }), _jsx(FlexDistributionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), isFlexWrapped ? (_jsx(FlexGapField, { property: lineGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit })) : null] })] })] })] })) : null, isFlexChild ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-child", children: [_jsx("div", { className: "editor__title", children: "Flex Child" }), _jsxs("div", { className: "layout__flex-child-fields", children: [_jsx(FlexChildValueField, { label: "Grow", property: "flex-grow", presets: FLEX_GROW_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Shrink", property: "flex-shrink", presets: FLEX_SHRINK_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Basis", property: "flex-basis", presets: FLEX_BASIS_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] })) : null, isGridContainer || isGridChild ? (_jsx(GridSection, { domElement: el, showContainer: isGridContainer, showChild: isGridChild, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null, showPositionInsets ? (_jsx(PositionInsets, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null] })] }));
112
+ return (_jsxs("div", { className: "editor", "data-test": "layout-section", children: [_jsx("div", { className: "editor__title", children: "Layout" }), _jsxs("div", { className: "layout", children: [_jsxs("div", { className: "layout__basic", children: [_jsx(SizeSection, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsxs("div", { className: "layout__tool-row", children: [_jsx(LayoutDropdown, { property: "display", options: DISPLAY_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(LayoutDropdown, { property: "position", options: POSITION_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] }), isFlexContainer ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-container", children: [_jsxs("div", { className: "layout__flex-heading", children: [_jsx("div", { className: "editor__title", children: "Flex" }), _jsx(FlexSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), _jsxs("div", { className: "layout__flex-lower", children: [mixedFlexDirection ? (_jsx("div", { className: "layout__alignment-grid", "data-test": "layout-flex-axis-mixed", children: "Choose a shared flex direction to edit physical alignment." })) : (_jsx(FlexAlignmentGrid, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })), _jsxs("div", { className: "layout__gap-column", "data-test": "layout-gap", children: [_jsxs("div", { className: "layout__direction-tools", children: [_jsx(FlexDirectionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexWrapToggle, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), _jsxs("div", { className: "layout__gap-fields", children: [_jsxs("div", { className: "layout__spacing-primary", children: [_jsx(FlexGapField, { property: relevantGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit }), _jsx(FlexDistributionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), isFlexWrapped ? (_jsx("div", { className: "layout__spacing-primary", children: _jsx(FlexGapField, { property: lineGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit }) })) : null] })] })] })] })) : null, isFlexChild ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-child", children: [_jsx("div", { className: "editor__title", children: "Flex Child" }), _jsxs("div", { className: "layout__flex-child-fields", children: [_jsx(FlexChildValueField, { label: "Grow", property: "flex-grow", presets: FLEX_GROW_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Shrink", property: "flex-shrink", presets: FLEX_SHRINK_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Basis", property: "flex-basis", presets: FLEX_BASIS_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] })) : null, isGridContainer || isGridChild ? (_jsx(GridSection, { domElement: el, showContainer: isGridContainer, showChild: isGridChild, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null, showPositionInsets ? (_jsx(PositionInsets, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null] })] }));
113
113
  }
114
114
  function SizeSection({ domElement: el, editTarget, selection, entries, tokenRows, revision, onAfterEdit }) {
115
115
  const [expanded, setExpanded] = useState(false);
@@ -187,7 +187,7 @@ function FlexWrapToggle({ domElement, editTarget, selection, revision = 0, onAft
187
187
  setStyle(editTarget ?? domElement, "flex-wrap", isWrapped ? "nowrap" : "wrap");
188
188
  onAfterEdit?.();
189
189
  }
190
- return (_jsx(Tooltip, { content: inlineStyleWarningContent(blockedBy), children: _jsx(IconButton, { variant: "secondary", size: "default", "data-active": isWrapped, "data-test": "layout-flex-wrap-toggle", label: mixed ? "Set Flex Wrap" : isWrapped ? "Disable Flex Wrap" : "Enable Flex Wrap", "aria-pressed": mixed ? "mixed" : isWrapped, disabled: blockedBy !== null, onClick: toggleWrap, children: _jsx(IconTextWrap, { size: 16, stroke: 1.8, "aria-hidden": "true" }) }) }));
190
+ return (_jsx(Tooltip, { content: inlineStyleWarningContent(blockedBy), children: _jsx(IconButton, { variant: "quiet", size: "default", "data-active": isWrapped, "data-test": "layout-flex-wrap-toggle", label: mixed ? "Set Flex Wrap" : isWrapped ? "Disable Flex Wrap" : "Enable Flex Wrap", "aria-pressed": mixed ? "mixed" : isWrapped, disabled: blockedBy !== null, onClick: toggleWrap, children: _jsx(IconTextWrap, { size: 16, stroke: 1.8, "aria-hidden": "true" }) }) }));
191
191
  }
192
192
  function FlexSettingsMenu({ domElement, editTarget, selection, revision = 0, onAfterEdit }) {
193
193
  const [direction] = useComputedLayoutValue(domElement, "flex-direction", "row", revision);
@@ -0,0 +1,42 @@
1
+ import type { ResolvedProperty } from "../../css/model/index.js";
2
+ import type { StyleDeclaration } from "../tokens/editActions.js";
3
+ export declare const BORDER_STYLES: readonly ["none", "hidden", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"];
4
+ export declare const BORDER_SIDES: readonly ["top", "right", "bottom", "left"];
5
+ export type BorderSide = (typeof BORDER_SIDES)[number];
6
+ export type BorderComponent = "width" | "style" | "color";
7
+ /** Reads a property's computed value for the inspected style state. */
8
+ export type ComputedStyleReader = (property: string, fallback: string) => string;
9
+ /** What the border editor knows about one element: its cascade rows and computed style. */
10
+ export interface BorderSource {
11
+ readonly rows: readonly ResolvedProperty[];
12
+ readonly computed: ComputedStyleReader;
13
+ }
14
+ export interface BorderEditorState {
15
+ /** Every side shares its width, style, and color. Never true for a group selection. */
16
+ readonly sidesLinked: boolean;
17
+ /** An unstructured `border` shorthand row that only a raw field can edit. */
18
+ readonly rawShorthandRow: ResolvedProperty | null;
19
+ /** The linked style draws a line, so linked width and color are meaningful. */
20
+ readonly linkedStyleDraws: boolean;
21
+ /** Rows backing the linked `border-width`, `border-style`, and `border-color` fields. */
22
+ readonly linkedRows: Readonly<Record<BorderComponent, ResolvedProperty | null>>;
23
+ }
24
+ /** User choices the border editor turns into declarations. */
25
+ export type BorderIntent = {
26
+ readonly kind: "add";
27
+ } | {
28
+ readonly kind: "remove";
29
+ } | {
30
+ readonly kind: "link-sides";
31
+ } | {
32
+ readonly kind: "set-style";
33
+ readonly property: string;
34
+ readonly style: string;
35
+ };
36
+ export declare function borderProperty(component: BorderComponent, side?: BorderSide): string;
37
+ export declare function readBorderEditor(source: BorderSource, { group }: {
38
+ readonly group: boolean;
39
+ }): BorderEditorState;
40
+ /** Whether any side draws a visible line, including after stylesheet resets. */
41
+ export declare function hasVisibleBorder(computed: ComputedStyleReader): boolean;
42
+ export declare function borderDeclarations(source: BorderSource, intent: BorderIntent): StyleDeclaration[];