@agent-native/core 0.98.14 → 0.98.16

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 (28) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +41 -7
  5. package/corpus/core/src/server/agent-chat-plugin.ts +11 -2
  6. package/corpus/templates/analytics/changelog/2026-07-13-ask-app-now-uses-the-current-core-agent-loop-and-response-gu.md +6 -0
  7. package/corpus/templates/analytics/netlify.toml +5 -1
  8. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +23 -11
  9. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +34 -3
  10. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +152 -62
  11. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +7 -7
  12. package/corpus/templates/design/app/global.css +2 -2
  13. package/corpus/templates/design/changelog/2026-07-10-canvas-size-and-option-hover-measurements.md +6 -0
  14. package/corpus/templates/design/changelog/2026-07-10-short-hex-colors-now-expand-like-figma.md +6 -0
  15. package/dist/agent/production-agent.d.ts.map +1 -1
  16. package/dist/agent/production-agent.js +34 -7
  17. package/dist/agent/production-agent.js.map +1 -1
  18. package/dist/collab/awareness.d.ts +2 -2
  19. package/dist/collab/awareness.d.ts.map +1 -1
  20. package/dist/collab/struct-routes.d.ts +1 -1
  21. package/dist/observability/routes.d.ts +3 -3
  22. package/dist/progress/routes.d.ts +1 -1
  23. package/dist/resources/handlers.d.ts +3 -3
  24. package/dist/secrets/routes.d.ts +3 -3
  25. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  26. package/dist/server/agent-chat-plugin.js +9 -2
  27. package/dist/server/agent-chat-plugin.js.map +1 -1
  28. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2239
31
- - template files: 5515
31
+ - template files: 5518
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.98.16
4
+
5
+ ### Patch Changes
6
+
7
+ - cc35446: Resolve the authenticated owner's active provider key for A2A and MCP ask-agent runs, matching the interactive chat engine path.
8
+
9
+ ## 0.98.15
10
+
11
+ ### Patch Changes
12
+
13
+ - 2161d10: Preserve streamed tool calls when an engine omits its normalized assistant-content event so delegated and MCP agent runs can still execute requested actions.
14
+
3
15
  ## 0.98.14
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.98.14",
3
+ "version": "0.98.16",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -3081,6 +3081,8 @@ export async function runAgentLoop(opts: {
3081
3081
 
3082
3082
  let assistantContent: EngineContentPart[] | undefined;
3083
3083
  let streamedAssistantText = "";
3084
+ const streamedAssistantToolCalls: import("./engine/types.js").EngineToolCallPart[] =
3085
+ [];
3084
3086
  let terminalStopReason:
3085
3087
  | Extract<EngineEvent, { type: "stop" }>["reason"]
3086
3088
  | undefined;
@@ -3122,6 +3124,7 @@ export async function runAgentLoop(opts: {
3122
3124
  for (let retry = 0; ; retry++) {
3123
3125
  assistantContent = undefined;
3124
3126
  streamedAssistantText = "";
3127
+ streamedAssistantToolCalls.length = 0;
3125
3128
  terminalStopReason = undefined;
3126
3129
  toolCallErrors.clear();
3127
3130
  try {
@@ -3465,7 +3468,16 @@ export async function runAgentLoop(opts: {
3465
3468
  } else if (event.type === "gateway-heartbeat") {
3466
3469
  send({ type: "stream_keepalive" });
3467
3470
  } else if (event.type === "tool-call") {
3468
- // The authoritative tool-call blocks arrive in assistant-content.
3471
+ // Most engines repeat the authoritative call in assistant-content,
3472
+ // but delegated/custom engines can expose only the stream event.
3473
+ // Keep it so the loop can still execute the requested action when
3474
+ // the normalized terminal event is missing.
3475
+ streamedAssistantToolCalls.push({
3476
+ type: "tool-call",
3477
+ id: event.id,
3478
+ name: event.name,
3479
+ input: event.input,
3480
+ });
3469
3481
  } else if (event.type === "tool-call-error") {
3470
3482
  toolCallErrors.set(event.id, {
3471
3483
  name: event.name,
@@ -3553,12 +3565,34 @@ export async function runAgentLoop(opts: {
3553
3565
  assistantContent = [];
3554
3566
  }
3555
3567
 
3556
- if (!assistantContent && streamedAssistantText.trim()) {
3557
- // Some delegated/custom engine streams emit text deltas and a terminal
3558
- // stop without the normalized assistant-content event. Preserve the
3559
- // streamed text so final-response guards still run; otherwise an app
3560
- // guard (for example Analytics' real-data guard) is silently bypassed.
3561
- assistantContent = [{ type: "text", text: streamedAssistantText }];
3568
+ if (
3569
+ (!assistantContent || assistantContent.length === 0) &&
3570
+ (streamedAssistantText.trim() || streamedAssistantToolCalls.length > 0)
3571
+ ) {
3572
+ // Some delegated/custom engine streams emit text deltas and/or tool-call
3573
+ // events followed by a terminal stop without the normalized
3574
+ // assistant-content event. Preserve both so final-response guards still
3575
+ // run and requested actions are not silently dropped.
3576
+ assistantContent = [
3577
+ ...(streamedAssistantText.trim()
3578
+ ? [{ type: "text" as const, text: streamedAssistantText }]
3579
+ : []),
3580
+ ...streamedAssistantToolCalls,
3581
+ ];
3582
+ } else if (assistantContent && streamedAssistantToolCalls.length > 0) {
3583
+ const existingToolCallIds = new Set(
3584
+ assistantContent
3585
+ .filter(
3586
+ (part): part is import("./engine/types.js").EngineToolCallPart =>
3587
+ part.type === "tool-call",
3588
+ )
3589
+ .map((part) => part.id),
3590
+ );
3591
+ for (const toolCall of streamedAssistantToolCalls) {
3592
+ if (!existingToolCallIds.has(toolCall.id)) {
3593
+ assistantContent.push(toolCall);
3594
+ }
3595
+ }
3562
3596
  }
3563
3597
  if (!assistantContent) {
3564
3598
  // No content — done
@@ -1252,9 +1252,12 @@ export function createAgentChatPlugin(
1252
1252
 
1253
1253
  // Use the SAME agent setup as the interactive chat — identical tools,
1254
1254
  // prompt, and capabilities. The A2A agent IS the app's agent.
1255
+ const { getOwnerActiveApiKey } =
1256
+ await import("../agent/production-agent.js");
1257
+ const ownerApiKey = await getOwnerActiveApiKey(userEmail);
1255
1258
  const a2aEngine = await resolveEngine({
1256
1259
  engineOption: options?.engine,
1257
- apiKey: options?.apiKey,
1260
+ apiKey: ownerApiKey ?? options?.apiKey,
1258
1261
  appId: options?.appId,
1259
1262
  });
1260
1263
 
@@ -1524,9 +1527,15 @@ export function createAgentChatPlugin(
1524
1527
  ? { externalAgents: options.externalAgents }
1525
1528
  : {}),
1526
1529
  askAgent: async (message: string) => {
1530
+ const ownerEmail = getRequestUserEmail();
1531
+ const { getOwnerActiveApiKey } =
1532
+ await import("../agent/production-agent.js");
1533
+ const ownerApiKey = ownerEmail
1534
+ ? await getOwnerActiveApiKey(ownerEmail)
1535
+ : undefined;
1527
1536
  const mcpEngine = await resolveEngine({
1528
1537
  engineOption: options?.engine,
1529
- apiKey: options?.apiKey,
1538
+ apiKey: ownerApiKey ?? options?.apiKey,
1530
1539
  appId: options?.appId,
1531
1540
  });
1532
1541
  const mcpModelCandidate =
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-13
4
+ ---
5
+
6
+ Ask app now uses the current core agent loop and response guards in production MCP runs.
@@ -1,6 +1,10 @@
1
1
  [build]
2
2
  ignore = "node ./scripts/netlify-ignore-build.mjs analytics"
3
- command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && NITRO_PRESET=netlify pnpm --filter analytics build"
3
+ # The workspace package dist folders are ignored/generated artifacts. Rebuild
4
+ # core explicitly here so a cached Netlify install cannot bundle an older agent
5
+ # loop into Analytics' server functions (including A2A/MCP final-response
6
+ # guards).
7
+ command = "export DATABASE_URL=${NETLIFY_DATABASE_URL:-$DATABASE_URL} && pnpm install && pnpm --filter @agent-native/core build && NITRO_PRESET=netlify pnpm --filter analytics build"
4
8
  publish = "templates/analytics/dist"
5
9
  functions = "templates/analytics/.netlify/functions-internal"
6
10
 
@@ -5046,17 +5046,29 @@ function SingleScreenCreationOverlay({
5046
5046
  />
5047
5047
  ) : null}
5048
5048
  {previewRect ? (
5049
- <div
5050
- data-creation-preview-rect
5051
- className="pointer-events-none absolute rounded-[2px] border-[1.5px] border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)]/10"
5052
- style={{
5053
- left: previewRect.x,
5054
- top: previewRect.y,
5055
- width: Math.max(1, previewRect.width),
5056
- height: Math.max(1, previewRect.height),
5057
- borderRadius: tool === "ellipse" ? "9999px" : undefined,
5058
- }}
5059
- />
5049
+ <>
5050
+ <div
5051
+ data-creation-preview-rect
5052
+ className="pointer-events-none absolute rounded-[2px] border-[1.5px] border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)]/10"
5053
+ style={{
5054
+ left: previewRect.x,
5055
+ top: previewRect.y,
5056
+ width: Math.max(1, previewRect.width),
5057
+ height: Math.max(1, previewRect.height),
5058
+ borderRadius: tool === "ellipse" ? "9999px" : undefined,
5059
+ }}
5060
+ />
5061
+ <span
5062
+ data-creation-preview-size
5063
+ className="pointer-events-none absolute z-10 -translate-x-1/2 translate-y-1 rounded bg-[var(--design-editor-accent-color)] px-1.5 py-0.5 text-[10px] font-medium leading-none text-[var(--design-editor-accent-contrast-color)] shadow-sm"
5064
+ style={{
5065
+ left: previewRect.x + previewRect.width / 2,
5066
+ top: previewRect.y + previewRect.height,
5067
+ }}
5068
+ >
5069
+ {Math.round(previewRect.width)} × {Math.round(previewRect.height)}
5070
+ </span>
5071
+ </>
5060
5072
  ) : null}
5061
5073
  {previewLine ? (
5062
5074
  <svg
@@ -7812,7 +7812,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7812
7812
  )),
7813
7813
  )}
7814
7814
 
7815
- {/* Figma-parity alt-hover measurement: red edge-to-edge distance
7815
+ {/* Figma-parity alt-hover measurement: orange edge-to-edge distance
7816
7816
  lines between the current selection and whatever frame/draft is
7817
7817
  under the cursor while Alt is held (pure hover, no drag). */}
7818
7818
  {[altHoverMeasurement?.horizontal, altHoverMeasurement?.vertical]
@@ -7822,7 +7822,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7822
7822
  .map((line) => (
7823
7823
  <span
7824
7824
  key={`alt-hover-${line.orientation}`}
7825
- className="pointer-events-none absolute z-40 bg-destructive"
7825
+ className="pointer-events-none absolute z-40 bg-[var(--design-editor-measure-color)]"
7826
7826
  style={
7827
7827
  line.orientation === "vertical"
7828
7828
  ? {
@@ -7857,7 +7857,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7857
7857
  return (
7858
7858
  <span
7859
7859
  key={`alt-hover-label-${line.orientation}`}
7860
- className="pointer-events-none absolute z-40 -translate-x-1/2 -translate-y-1/2 rounded bg-destructive px-1 py-0.5 text-[10px] font-medium leading-none text-destructive-foreground shadow-sm"
7860
+ className="pointer-events-none absolute z-40 -translate-x-1/2 -translate-y-1/2 rounded bg-[var(--design-editor-measure-color)] px-1 py-0.5 text-[10px] font-medium leading-none text-white shadow-sm"
7861
7861
  style={{
7862
7862
  left: pan.x + (SURFACE_PADDING + labelCanvasPoint.x) * scale,
7863
7863
  top: pan.y + (SURFACE_PADDING + labelCanvasPoint.y) * scale,
@@ -7868,6 +7868,37 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
7868
7868
  );
7869
7869
  })}
7870
7870
 
7871
+ {/* Live width × height readout while drawing, pinned below the draft box.
7872
+ Rendered outside the transformed world so it stays a fixed size.
7873
+ Skipped for line/arrow/pen and the pre-drag zero-size state. */}
7874
+ {creationPreview &&
7875
+ creationPreview.tool !== "line" &&
7876
+ creationPreview.tool !== "arrow" &&
7877
+ creationPreview.tool !== "pen" &&
7878
+ creationPreview.geometry.width > 0 &&
7879
+ creationPreview.geometry.height > 0 ? (
7880
+ <span
7881
+ className="pointer-events-none absolute z-40 -translate-x-1/2 translate-y-1 rounded bg-[var(--design-editor-accent-color)] px-1.5 py-0.5 text-[10px] font-medium leading-none text-[var(--design-editor-accent-contrast-color)] shadow-sm"
7882
+ style={{
7883
+ left:
7884
+ pan.x +
7885
+ (SURFACE_PADDING +
7886
+ creationPreview.geometry.x +
7887
+ creationPreview.geometry.width / 2) *
7888
+ scale,
7889
+ top:
7890
+ pan.y +
7891
+ (SURFACE_PADDING +
7892
+ creationPreview.geometry.y +
7893
+ creationPreview.geometry.height) *
7894
+ scale,
7895
+ }}
7896
+ >
7897
+ {Math.round(creationPreview.geometry.width)} ×{" "}
7898
+ {Math.round(creationPreview.geometry.height)}
7899
+ </span>
7900
+ ) : null}
7901
+
7871
7902
  {/* Equal-gap distance labels render outside the pan/scale-transformed
7872
7903
  world container (same reasoning as the marquee/duplicate-preview
7873
7904
  overlays above it) so they need the explicit
@@ -100,6 +100,7 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
100
100
  "",
101
101
  );
102
102
  chromeTransitionStyle.textContent =
103
+ "html{overflow:clip}" /* prevent negative-offset handles from expanding the iframe */ +
103
104
  '[data-agent-native-edit-overlay="selection"]{transition:border-width 150ms ease-out}' +
104
105
  '[data-agent-native-empty-text-editing="true"] [data-agent-native-edit-overlay="selection"]{display:none!important}' +
105
106
  "[data-agent-native-text-editing]{outline:none!important;outline-offset:0!important}" +
@@ -1844,17 +1845,46 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
1844
1845
  }
1845
1846
  selectionOverlay.appendChild(handle);
1846
1847
  });
1847
- ["nw", "ne", "se", "sw"].forEach(function (pos) {
1848
- var rotate = document.createElement("span");
1849
- rotate.setAttribute("data-agent-native-rotate-handle", pos);
1850
- rotate.style.cssText =
1851
- "position:absolute;width:18px;height:18px;border-radius:999px;pointer-events:auto;cursor:grab;";
1852
- if (pos.indexOf("n") !== -1) rotate.style.top = "-26px";
1853
- if (pos.indexOf("s") !== -1) rotate.style.bottom = "-26px";
1854
- if (pos.indexOf("w") !== -1) rotate.style.left = "-26px";
1855
- if (pos.indexOf("e") !== -1) rotate.style.right = "-26px";
1856
- selectionOverlay.appendChild(rotate);
1857
- });
1848
+ (function () {
1849
+ var baseAngles = { nw: 270, ne: 0, se: 90, sw: 180 };
1850
+ function rotateCursorUri(angleDeg) {
1851
+ var svg =
1852
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">' +
1853
+ '<g transform="rotate(' +
1854
+ angleDeg +
1855
+ ' 10 10)" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">' +
1856
+ '<path d="M 4 8 A 7 7 0 0 1 16 8" stroke="white" stroke-width="3.5"/>' +
1857
+ '<path d="M 4 8 A 7 7 0 0 1 16 8"/>' +
1858
+ '<path d="M 12.5 3.5 L 16 8 L 11 8.5" fill="white" stroke="white" stroke-width="3.5" stroke-linejoin="round"/>' +
1859
+ '<path d="M 12.5 3.5 L 16 8 L 11 8.5" fill="black"/>' +
1860
+ "</g></svg>";
1861
+ return (
1862
+ 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '") 10 10, grab'
1863
+ );
1864
+ }
1865
+ ["nw", "ne", "se", "sw"].forEach(function (pos) {
1866
+ var handle = document.createElement("span");
1867
+ handle.setAttribute("data-agent-native-rotate-handle", pos);
1868
+ handle.style.cssText =
1869
+ "position:absolute;width:28px;height:28px;border-radius:999px;pointer-events:auto;";
1870
+ handle.style.cursor = rotateCursorUri(baseAngles[pos]);
1871
+ if (pos.indexOf("n") !== -1) handle.style.top = "-34px";
1872
+ if (pos.indexOf("s") !== -1) handle.style.bottom = "-34px";
1873
+ if (pos.indexOf("w") !== -1) handle.style.left = "-34px";
1874
+ if (pos.indexOf("e") !== -1) handle.style.right = "-34px";
1875
+ selectionOverlay.appendChild(handle);
1876
+ });
1877
+ var button = document.createElement("span");
1878
+ button.setAttribute("data-agent-native-rotate-handle", "top-center");
1879
+ button.style.cssText =
1880
+ "position:absolute;left:50%;transform:translateX(-50%);top:-22px;" +
1881
+ "width:16px;height:16px;pointer-events:auto;" +
1882
+ "display:flex;align-items:center;justify-content:center;" +
1883
+ "border-radius:999px;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.3);" +
1884
+ "cursor:grab;user-select:none;font-size:10px;line-height:1;color:#333;";
1885
+ button.textContent = "↻";
1886
+ selectionOverlay.appendChild(button);
1887
+ })();
1858
1888
  var spacingOverlay = document.createElement("div");
1859
1889
  spacingOverlay.setAttribute("data-agent-native-spacing-overlay", "");
1860
1890
  spacingOverlay.style.cssText =
@@ -1967,6 +1997,12 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
1967
1997
 
1968
1998
  var measurementOverlay = document.createElement("div");
1969
1999
  measurementOverlay.setAttribute("data-agent-native-measurement-overlay", "");
2000
+ // Tag as an edit overlay so the content-replacement path preserves it (only
2001
+ // [data-agent-native-edit-overlay] nodes survive a body rebuild).
2002
+ measurementOverlay.setAttribute(
2003
+ "data-agent-native-edit-overlay",
2004
+ "measurement",
2005
+ );
1970
2006
  measurementOverlay.style.cssText =
1971
2007
  "position:fixed;inset:0;z-index:100001;display:none;pointer-events:none;color:var(--design-editor-measure-color);font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;";
1972
2008
  document.body.appendChild(measurementOverlay);
@@ -3845,50 +3881,44 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
3845
3881
  .querySelectorAll("[data-agent-native-rotate-handle]")
3846
3882
  .forEach(function (handle) {
3847
3883
  var pos = handle.getAttribute("data-agent-native-rotate-handle") || "";
3848
- handle.style.width = 18 * sx + "px";
3849
- handle.style.height = 18 * sy + "px";
3850
- if (pos.indexOf("n") !== -1) handle.style.top = -26 * sy + "px";
3851
- if (pos.indexOf("s") !== -1) handle.style.bottom = -26 * sy + "px";
3852
- if (pos.indexOf("w") !== -1) handle.style.left = -26 * sx + "px";
3853
- if (pos.indexOf("e") !== -1) handle.style.right = -26 * sx + "px";
3884
+ if (pos === "top-center") {
3885
+ var buttonScale = Math.min(sx, sy);
3886
+ handle.style.width = 16 * buttonScale + "px";
3887
+ handle.style.height = 16 * buttonScale + "px";
3888
+ handle.style.fontSize = 10 * buttonScale + "px";
3889
+ handle.style.top = -22 * sy + "px";
3890
+ return;
3891
+ }
3892
+ var size = Math.min(sx, sy);
3893
+ handle.style.width = 28 * size + "px";
3894
+ handle.style.height = 28 * size + "px";
3895
+ if (pos.indexOf("n") !== -1) handle.style.top = -34 * sy + "px";
3896
+ if (pos.indexOf("s") !== -1) handle.style.bottom = -34 * sy + "px";
3897
+ if (pos.indexOf("w") !== -1) handle.style.left = -34 * sx + "px";
3898
+ if (pos.indexOf("e") !== -1) handle.style.right = -34 * sx + "px";
3854
3899
  });
3855
3900
  }
3856
3901
 
3857
- // Rotation-aware local-box placement shared by selectionOverlay, the hover
3858
- // highlightOverlay, and the passive multi-selection overlays: prefer the CSS
3859
- // box + rotation transform so the outline hugs the rotated element rather
3860
- // than its inflated axis-aligned bounding box. Returns true when it placed
3861
- // the overlay (caller should skip the AABB fallback), false when the element
3862
- // has no usable local box (falls back to getBoundingClientRect).
3902
+ // Returns true when the overlay was placed with the element's rotated CSS box.
3863
3903
  function positionOverlayForRotatedLocalBox(
3864
3904
  overlay: HTMLElement,
3865
3905
  el: Element,
3866
3906
  ): boolean {
3867
3907
  var elCs = window.getComputedStyle(el);
3868
- var elLeft = readFinitePx(el.style.left || elCs.left);
3869
- var elTop = readFinitePx(el.style.top || elCs.top);
3870
- var elW = readFinitePx(el.style.width || elCs.width);
3871
- var elH = readFinitePx(el.style.height || elCs.height);
3908
+ var elW = readFinitePx((el as HTMLElement).style.width || elCs.width);
3909
+ var elH = readFinitePx((el as HTMLElement).style.height || elCs.height);
3872
3910
  var elRot = currentRotation(el);
3873
- var canUseLocalBox =
3874
- Math.abs(elRot) > 0.01 &&
3875
- elLeft !== null &&
3876
- elTop !== null &&
3877
- elW !== null &&
3878
- elH !== null;
3879
- if (!canUseLocalBox) return false;
3880
- // Convert element-local left/top to viewport coords by walking to the
3881
- // nearest positioned ancestor (same reference frame as getBoundingClientRect).
3882
- var parentRect = (
3883
- (el as HTMLElement).offsetParent || document.documentElement
3884
- ).getBoundingClientRect();
3911
+ if (Math.abs(elRot) < 0.01 || elW === null || elH === null) return false;
3912
+ var rect = (el as HTMLElement).getBoundingClientRect();
3913
+ var cx = rect.left + rect.width / 2;
3914
+ var cy = rect.top + rect.height / 2;
3885
3915
  overlay.style.display = "block";
3886
- overlay.style.left = parentRect.left + elLeft + "px";
3887
- overlay.style.top = parentRect.top + elTop + "px";
3916
+ overlay.style.left = cx - elW / 2 + "px";
3917
+ overlay.style.top = cy - elH / 2 + "px";
3888
3918
  overlay.style.width = elW + "px";
3889
3919
  overlay.style.height = elH + "px";
3890
3920
  overlay.style.transform = "rotate(" + elRot + "deg)";
3891
- overlay.style.transformOrigin = "0 0";
3921
+ overlay.style.transformOrigin = "50% 50%";
3892
3922
  return true;
3893
3923
  }
3894
3924
 
@@ -4212,6 +4242,11 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
4212
4242
  }
4213
4243
  var selectedRect = a.getBoundingClientRect();
4214
4244
  var hoverRect = b.getBoundingClientRect();
4245
+ // A content re-render can rebuild document.body and drop this overlay;
4246
+ // re-attach it before drawing so the lines always render.
4247
+ if (!measurementOverlay.isConnected) {
4248
+ document.body.appendChild(measurementOverlay);
4249
+ }
4215
4250
  measurementOverlay.innerHTML = "";
4216
4251
  measurementOverlay.style.display = "block";
4217
4252
 
@@ -5631,32 +5666,66 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
5631
5666
  },
5632
5667
  );
5633
5668
 
5634
- function currentRotation(el) {
5635
- var transform =
5636
- el.style.transform || window.getComputedStyle(el).transform || "";
5637
- var match = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
5669
+ function rotationFromTransform(transform) {
5670
+ var match = transform.match(/rotate(?:Z)?\((-?\d+(?:\.\d+)?)deg\)/i);
5638
5671
  if (match) return parseFloat(match[1]) || 0;
5639
5672
  if (transform && transform !== "none" && window.DOMMatrixReadOnly) {
5640
5673
  try {
5641
5674
  var matrix = new DOMMatrixReadOnly(transform);
5642
- return Math.round((Math.atan2(matrix.b, matrix.a) * 180) / Math.PI);
5675
+ return (Math.atan2(matrix.b, matrix.a) * 180) / Math.PI;
5643
5676
  } catch (err) {}
5644
5677
  }
5645
5678
  return 0;
5646
5679
  }
5647
5680
 
5648
- function mergeRotation(el, degrees) {
5649
- var inline = el.style.transform || "";
5650
- var next = inline.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/)
5651
- ? inline.replace(
5652
- /rotate\((-?\d+(?:\.\d+)?)deg\)/,
5653
- "rotate(" + degrees + "deg)",
5654
- )
5655
- : (inline && inline !== "none" ? inline + " " : "") +
5656
- "rotate(" +
5657
- degrees +
5658
- "deg)";
5659
- return next.trim();
5681
+ function independentRotation(rotate) {
5682
+ var match = rotate.match(
5683
+ /^(?:z\s+)?(-?\d+(?:\.\d+)?)(deg|grad|rad|turn)$/i,
5684
+ );
5685
+ if (!match) return 0;
5686
+ var value = parseFloat(match[1]) || 0;
5687
+ var unit = match[2].toLowerCase();
5688
+ if (unit === "grad") return value * 0.9;
5689
+ if (unit === "rad") return (value * 180) / Math.PI;
5690
+ if (unit === "turn") return value * 360;
5691
+ return value;
5692
+ }
5693
+
5694
+ function currentRotation(el) {
5695
+ var computed = window.getComputedStyle(el);
5696
+ return (
5697
+ rotationFromTransform(computed.transform || "") +
5698
+ independentRotation(computed.rotate || "")
5699
+ );
5700
+ }
5701
+
5702
+ // Merge an ABSOLUTE rotation value (degrees) into a transform string.
5703
+ // When the string already has a rotate/rotateZ(), replace it in place.
5704
+ // When the transform is a matrix() (e.g. computed from a class rule), strip
5705
+ // the rotation component and append the new absolute rotate() so the inline
5706
+ // value only adds what the class doesn't already own as non-rotation parts.
5707
+ // When the string has non-rotation functions (translate, scale, etc.) without
5708
+ // an explicit rotate(), append rotate() so other transforms are preserved.
5709
+ function mergeAbsoluteRotation(transform, degrees) {
5710
+ var rotatePattern = /rotate(?:Z)?\((-?\d+(?:\.\d+)?)deg\)/i;
5711
+ if (rotatePattern.test(transform)) {
5712
+ return transform
5713
+ .replace(rotatePattern, "rotate(" + degrees + "deg)")
5714
+ .trim();
5715
+ }
5716
+ // matrix() is the computed form of a class-rule transform; writing it
5717
+ // inline would hard-pin every property from that rule. Instead start fresh
5718
+ // with just the target rotation so the class still owns everything else.
5719
+ if (/^matrix(?:3d)?\(/i.test(transform.trim())) {
5720
+ return "rotate(" + degrees + "deg)";
5721
+ }
5722
+ // transform string with other functions but no existing rotate: append.
5723
+ return (
5724
+ (transform && transform !== "none" ? transform + " " : "") +
5725
+ "rotate(" +
5726
+ degrees +
5727
+ "deg)"
5728
+ ).trim();
5660
5729
  }
5661
5730
 
5662
5731
  function ensurePositionable(el) {
@@ -9115,6 +9184,11 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
9115
9184
  // null-deref in onMove/onUp.
9116
9185
  var rotateEl = selectedEl;
9117
9186
  var originalInlineTransform = rotateEl.style.transform;
9187
+ var originalComputedTransform = window.getComputedStyle(rotateEl).transform;
9188
+ var baseTransform =
9189
+ originalInlineTransform && originalInlineTransform !== "none"
9190
+ ? originalInlineTransform
9191
+ : originalComputedTransform;
9118
9192
  function onMove(ev) {
9119
9193
  if (!rotateEl) return;
9120
9194
  var pointerAngle =
@@ -9123,7 +9197,7 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
9123
9197
  var next = originRotation + pointerAngle - originAngle;
9124
9198
  if (ev.shiftKey) next = Math.round(next / 15) * 15;
9125
9199
  next = Math.round(next);
9126
- rotateEl.style.transform = mergeRotation(rotateEl, next);
9200
+ rotateEl.style.transform = mergeAbsoluteRotation(baseTransform, next);
9127
9201
  showTransformBadge(next + "deg", ev.clientX, ev.clientY);
9128
9202
  refreshOverlays();
9129
9203
  }
@@ -10303,7 +10377,11 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
10303
10377
  } else {
10304
10378
  hideMeasurements();
10305
10379
  }
10306
- if (hoveredEl !== lastHoverInfoPostedEl) {
10380
+ // While Alt is held (measurement mode) keep hover local: posting it would
10381
+ // update the host's hoveredSelector, re-run replayIframeEditorState, and
10382
+ // echo selection/hover back every move — a loop that jitters selection
10383
+ // and flickers the measurement lines.
10384
+ if (!e.altKey && hoveredEl !== lastHoverInfoPostedEl) {
10307
10385
  lastHoverInfoPostedEl = hoveredEl;
10308
10386
  var info = getLightElementInfo(hoveredEl);
10309
10387
  (window.parent as Window).postMessage(
@@ -10365,7 +10443,19 @@ declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
10365
10443
  window.addEventListener(
10366
10444
  "keyup",
10367
10445
  function (e) {
10368
- if (e.key === "Alt") hideMeasurements();
10446
+ if (e.key === "Alt") {
10447
+ hideMeasurements();
10448
+ // Re-sync the hover suppressed during measurement so the host isn't
10449
+ // left on a stale element until the next pointermove.
10450
+ lastHoverInfoPostedEl = hoveredEl;
10451
+ (window.parent as Window).postMessage(
10452
+ {
10453
+ type: "element-hover",
10454
+ payload: hoveredEl ? getLightElementInfo(hoveredEl) : null,
10455
+ },
10456
+ "*",
10457
+ );
10458
+ }
10369
10459
  },
10370
10460
  true,
10371
10461
  );
@@ -2678,16 +2678,16 @@ export function parseNumericDraft(draft: string): number | null {
2678
2678
  }
2679
2679
 
2680
2680
  /**
2681
- * Expands a bare 1-digit hex fragment (e.g. "F") into the standard 3-digit
2682
- * shorthand ("FFF", which `parseCssColor` then doubles up to "FFFFFF") so a
2683
- * single typed hex digit still commits instead of being rejected as
2684
- * unparseable. Standard 3/4/6/8-digit hex (already handled by
2685
- * `parseCssColor`) passes through unchanged.
2681
+ * Expands 1-3 digit hex inputs to their 6-digit Figma-style equivalents.
2682
+ * One digit fills every channel, two digits repeat as a grayscale byte, and
2683
+ * three digits use standard CSS shorthand expansion.
2686
2684
  */
2687
2685
  export function expandHexShorthand(value: string): string {
2688
2686
  const trimmed = value.trim().replace(/^#/, "");
2689
- if (/^[0-9a-f]$/i.test(trimmed)) {
2690
- return trimmed.repeat(3);
2687
+ if (/^[0-9a-f]$/i.test(trimmed)) return trimmed.repeat(6);
2688
+ if (/^[0-9a-f]{2}$/i.test(trimmed)) return trimmed.repeat(3);
2689
+ if (/^[0-9a-f]{3}$/i.test(trimmed)) {
2690
+ return [...trimmed].map((digit) => digit.repeat(2)).join("");
2691
2691
  }
2692
2692
  return trimmed;
2693
2693
  }
@@ -79,7 +79,7 @@
79
79
  );
80
80
  --design-editor-accent-strong-color: var(--design-editor-accent-color);
81
81
  --design-editor-accent-contrast-color: #ffffff;
82
- --design-editor-measure-color: hsl(344 100% 63%);
82
+ --design-editor-measure-color: hsl(24 95% 53%);
83
83
  --design-editor-panel-bg: hsl(var(--background));
84
84
  --design-editor-panel-raised-bg: hsl(var(--secondary));
85
85
  --design-editor-control-bg: hsl(var(--muted));
@@ -162,7 +162,7 @@
162
162
  --design-editor-panel-divider-color: hsl(0 0% 22%);
163
163
  --design-editor-accent-strong-color: var(--design-editor-accent-color);
164
164
  --design-editor-accent-contrast-color: #ffffff;
165
- --design-editor-measure-color: hsl(344 100% 63%);
165
+ --design-editor-measure-color: hsl(24 95% 53%);
166
166
  --design-editor-panel-bg: hsl(0 0% 13%);
167
167
  --design-editor-panel-raised-bg: hsl(0 0% 18%);
168
168
  --design-editor-control-bg: hsl(0 0% 18%);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-10
4
+ ---
5
+
6
+ The canvas now shows a live size readout while you draw, and holding Option while hovering another element shows the distance between them.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-10
4
+ ---
5
+
6
+ Short hex colors now expand like Figma when committed.