@agent-native/core 0.131.3 → 0.131.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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/docs/content/docs-components.mdx +491 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +58 -34
- package/corpus/core/src/localization/default-messages.ts +6 -0
- package/corpus/templates/content/.agents/skills/content-product-development/SKILL.md +47 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -1
- package/corpus/templates/design/actions/get-design.ts +11 -3
- package/corpus/templates/design/actions/present-design-variants.ts +62 -5
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +64 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +199 -25
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +160 -46
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -1
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +59 -12
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +1 -1
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +34 -29
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +2 -2
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +28 -10
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +15 -28
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +4 -3
- package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +12 -8
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +34 -9
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +13 -27
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/zoom-gesture.ts +95 -0
- package/corpus/templates/design/app/i18n-data.ts +121 -10
- package/corpus/templates/design/app/pages/design-editor/data-operations.ts +45 -0
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +59 -0
- package/corpus/templates/design/app/pages/design-editor/text-edit-utils.ts +115 -40
- package/corpus/templates/design/changelog/2026-07-28-generated-design-variants-no-longer-overlap-each-other-on-th.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-auto-layout-direction-icons-show-an-arrow.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-breakpoints-no-longer-overlap-neighbouring-screens.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-canvas-is-more-responsive-when-selecting-and-adding-frames.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-canvas-zoom-is-smoother-and-labels-hold-their-size.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-double-click-a-frame-now-drills-into-its-layers.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-export-preview-shows-the-real-render.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-figma-parity-shortcuts.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-more-breakpoint-width-presets.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-resize-a-selected-frame-to-a-device-preset.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-text-tool-reaches-the-right-surface.md +6 -0
- package/corpus/templates/design/shared/canvas-frames.ts +23 -0
- package/corpus/templates/design/shared/code-layer.ts +54 -2
- package/corpus/templates/design/shared/responsive-frame-layout.ts +55 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +16 -5
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/localization/default-messages.d.ts +6 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +6 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/docs-components.mdx +491 -0
- package/package.json +1 -1
- package/src/client/AgentPanel.tsx +58 -34
- package/src/localization/default-messages.ts +6 -0
|
@@ -174,9 +174,11 @@ const EMPTY_SCREEN_IDS: readonly string[] = [];
|
|
|
174
174
|
// redeclared locally and drifting from the shared constant.
|
|
175
175
|
const MIN_ZOOM = DEFAULT_CANVAS_MIN_ZOOM;
|
|
176
176
|
const MAX_ZOOM = DEFAULT_CANVAS_MAX_ZOOM;
|
|
177
|
-
const ZOOM_SENSITIVITY = 0.01;
|
|
178
|
-
const MAX_WHEEL_ZOOM_DELTA = 120;
|
|
179
177
|
const MAX_WHEEL_PAN_DELTA = 140;
|
|
178
|
+
/** Live counter-scale for constant-size frame chrome, written on the world
|
|
179
|
+
* element every gesture frame by applyViewToDom. React supplies the same
|
|
180
|
+
* number as the var's fallback, so first paint and SSR are unaffected. */
|
|
181
|
+
const CHROME_SCALE_CSS_VAR = "--an-chrome-scale";
|
|
180
182
|
const PIXEL_GRID_ZOOM = 800;
|
|
181
183
|
import {
|
|
182
184
|
BOARD_SURFACE_BACKGROUND,
|
|
@@ -301,8 +303,13 @@ import {
|
|
|
301
303
|
previewDraftPrimitive,
|
|
302
304
|
shapeClosingHandles,
|
|
303
305
|
} from "./multi-screen/draft-primitives";
|
|
306
|
+
import {
|
|
307
|
+
drillInCandidateKey,
|
|
308
|
+
resolveDrillInTarget,
|
|
309
|
+
} from "./multi-screen/drill-in";
|
|
304
310
|
import {
|
|
305
311
|
angleBetween,
|
|
312
|
+
BREAKPOINT_FRAME_GAP,
|
|
306
313
|
cloneFrameGeometryById,
|
|
307
314
|
deviceViewportFloorForWidth,
|
|
308
315
|
findTopFrameEntryAtPoint,
|
|
@@ -340,6 +347,11 @@ import {
|
|
|
340
347
|
} from "./multi-screen/primitive-drop-target";
|
|
341
348
|
import type { AlignmentGuide, CanvasFrameEntry } from "./multi-screen/types";
|
|
342
349
|
import { vectorEditCanvasToLocalPoint } from "./multi-screen/vector-edit-geometry";
|
|
350
|
+
import {
|
|
351
|
+
isPinchZoomDelta,
|
|
352
|
+
resolveExternalZoomAnchor,
|
|
353
|
+
resolveZoomFactor,
|
|
354
|
+
} from "./multi-screen/zoom-gesture";
|
|
343
355
|
|
|
344
356
|
/**
|
|
345
357
|
* Imperatively writes a draft primitive's full visual state onto its cached
|
|
@@ -909,6 +921,17 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
909
921
|
// externally-driven (toolbar/keyboard) zoom change without a TDZ error.
|
|
910
922
|
const recomputePenPointerForViewChangeRef = useRef<() => void>(() => {});
|
|
911
923
|
const suppressNextPick = useRef(false);
|
|
924
|
+
/** Where the last frame-body double-click landed, so consecutive
|
|
925
|
+
* double-clicks descend one level further instead of re-selecting the same
|
|
926
|
+
* layer. Reset whenever the pointer moves to a different screen. */
|
|
927
|
+
const drillInTargetRef = useRef<{ screenId: string; key: string } | null>(
|
|
928
|
+
null,
|
|
929
|
+
);
|
|
930
|
+
/** Generation counter for drill-in requests. Collecting candidates is an
|
|
931
|
+
* async iframe round-trip, so ANY newer selection — another double-click, a
|
|
932
|
+
* plain click, a marquee — must invalidate an in-flight one, or its late
|
|
933
|
+
* reply overwrites the selection the user actually made. */
|
|
934
|
+
const drillInRequestRef = useRef(0);
|
|
912
935
|
const feedbackTimerRef = useRef<number | null>(null);
|
|
913
936
|
const pendingWheelGestureRef = useRef<PendingWheelGesture | null>(null);
|
|
914
937
|
const wheelGestureFrameRef = useRef<number | null>(null);
|
|
@@ -1319,7 +1342,10 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1319
1342
|
: referenceId
|
|
1320
1343
|
? frameGeometryRef.current[referenceId]
|
|
1321
1344
|
: undefined;
|
|
1322
|
-
|
|
1345
|
+
// A screen's frame is routinely much taller than the viewport, so its centre
|
|
1346
|
+
// is often far off-screen; anchoring there holds a point the user cannot see
|
|
1347
|
+
// and pushes the visible content out of frame entirely.
|
|
1348
|
+
const frameCenter = activeGeometry
|
|
1323
1349
|
? canvasToScreenPoint(
|
|
1324
1350
|
{
|
|
1325
1351
|
x: activeGeometry.x + activeGeometry.width / 2,
|
|
@@ -1329,9 +1355,11 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
1329
1355
|
{ x: 0, y: 0 },
|
|
1330
1356
|
SURFACE_PADDING,
|
|
1331
1357
|
)
|
|
1332
|
-
:
|
|
1333
|
-
|
|
1334
|
-
|
|
1358
|
+
: null;
|
|
1359
|
+
const cursor = resolveExternalZoomAnchor({
|
|
1360
|
+
frameCenter,
|
|
1361
|
+
surfaceSize: { width: rect?.width ?? 0, height: rect?.height ?? 0 },
|
|
1362
|
+
});
|
|
1335
1363
|
const nextPan = getPanForZoomToCursor({
|
|
1336
1364
|
pan: panRef.current,
|
|
1337
1365
|
cursor,
|
|
@@ -3364,6 +3392,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3364
3392
|
(e: React.MouseEvent) => {
|
|
3365
3393
|
e.preventDefault();
|
|
3366
3394
|
e.stopPropagation();
|
|
3395
|
+
// Supersede any in-flight drill-in: its reply must not overwrite this
|
|
3396
|
+
// gesture's selection (see drillInRequestRef).
|
|
3397
|
+
drillInRequestRef.current += 1;
|
|
3367
3398
|
const originCanvas = getCanvasPoint(e.clientX, e.clientY);
|
|
3368
3399
|
let latestRect = normalizeRectFromPoints(originCanvas, originCanvas);
|
|
3369
3400
|
let layerCandidates: CanvasLayerMarqueeCandidate[] = [];
|
|
@@ -6082,6 +6113,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6082
6113
|
suppressNextPick.current = false;
|
|
6083
6114
|
return;
|
|
6084
6115
|
}
|
|
6116
|
+
// Supersede any in-flight drill-in: its reply must not overwrite this pick.
|
|
6117
|
+
drillInRequestRef.current += 1;
|
|
6085
6118
|
|
|
6086
6119
|
if (e.shiftKey) {
|
|
6087
6120
|
updateSelectedDraftIds(() => []);
|
|
@@ -6115,7 +6148,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6115
6148
|
],
|
|
6116
6149
|
);
|
|
6117
6150
|
|
|
6118
|
-
const
|
|
6151
|
+
const handleFrameInteract = useCallback(
|
|
6119
6152
|
(id: string, e: React.MouseEvent<HTMLElement>) => {
|
|
6120
6153
|
e.preventDefault();
|
|
6121
6154
|
e.stopPropagation();
|
|
@@ -6137,6 +6170,65 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6137
6170
|
],
|
|
6138
6171
|
);
|
|
6139
6172
|
|
|
6173
|
+
/**
|
|
6174
|
+
* Figma parity: double-clicking a frame's body descends into its layers so
|
|
6175
|
+
* the inspector shows the thing under the pointer. It deliberately does NOT
|
|
6176
|
+
* enter Interact — that is the frame label's own button (onEdit). Locked
|
|
6177
|
+
* screens have no selectable children to descend into, so they keep the
|
|
6178
|
+
* Interact behavior.
|
|
6179
|
+
*/
|
|
6180
|
+
const handleFrameEnter = useCallback(
|
|
6181
|
+
(id: string, e: React.MouseEvent<HTMLElement>) => {
|
|
6182
|
+
e.preventDefault();
|
|
6183
|
+
e.stopPropagation();
|
|
6184
|
+
if (lockedScreenIdSet.has(id)) {
|
|
6185
|
+
onEdit?.(id);
|
|
6186
|
+
return;
|
|
6187
|
+
}
|
|
6188
|
+
const point = getCanvasPoint(e.clientX, e.clientY);
|
|
6189
|
+
const previousKey =
|
|
6190
|
+
drillInTargetRef.current?.screenId === id
|
|
6191
|
+
? drillInTargetRef.current.key
|
|
6192
|
+
: null;
|
|
6193
|
+
const requestId = drillInRequestRef.current + 1;
|
|
6194
|
+
drillInRequestRef.current = requestId;
|
|
6195
|
+
void collectLayerMarqueeCandidates(new Set([id])).then((candidates) => {
|
|
6196
|
+
if (drillInRequestRef.current !== requestId) return;
|
|
6197
|
+
const target = resolveDrillInTarget({
|
|
6198
|
+
candidates,
|
|
6199
|
+
screenId: id,
|
|
6200
|
+
point,
|
|
6201
|
+
previousKey,
|
|
6202
|
+
});
|
|
6203
|
+
if (!target) {
|
|
6204
|
+
// Nothing selectable under the pointer (e.g. an empty frame). Leave
|
|
6205
|
+
// the frame itself selected rather than falling back to Interact.
|
|
6206
|
+
drillInTargetRef.current = null;
|
|
6207
|
+
return;
|
|
6208
|
+
}
|
|
6209
|
+
drillInTargetRef.current = {
|
|
6210
|
+
screenId: id,
|
|
6211
|
+
key: drillInCandidateKey(target),
|
|
6212
|
+
};
|
|
6213
|
+
updateSelectedDraftIds(() => []);
|
|
6214
|
+
updateSelectedIds(() => []);
|
|
6215
|
+
onLayerMarqueeSelectionChange?.(
|
|
6216
|
+
[{ screenId: target.screenId, info: target.info }],
|
|
6217
|
+
{ source: "pointer" },
|
|
6218
|
+
);
|
|
6219
|
+
});
|
|
6220
|
+
},
|
|
6221
|
+
[
|
|
6222
|
+
collectLayerMarqueeCandidates,
|
|
6223
|
+
getCanvasPoint,
|
|
6224
|
+
lockedScreenIdSet,
|
|
6225
|
+
onEdit,
|
|
6226
|
+
onLayerMarqueeSelectionChange,
|
|
6227
|
+
updateSelectedDraftIds,
|
|
6228
|
+
updateSelectedIds,
|
|
6229
|
+
],
|
|
6230
|
+
);
|
|
6231
|
+
|
|
6140
6232
|
const beginDuplicateGesture = useCallback(
|
|
6141
6233
|
(screen: ScreenFile, display: string, e: React.MouseEvent<HTMLElement>) => {
|
|
6142
6234
|
if (readOnly) return;
|
|
@@ -6558,6 +6650,16 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6558
6650
|
// 2D translate (not translate3d) so the layer is not GPU-pinned to a
|
|
6559
6651
|
// stale low-res raster — keeps zoomed-in content crisp.
|
|
6560
6652
|
world.style.transform = `translate(${p.x}px, ${p.y}px) scale(${nextScale})`;
|
|
6653
|
+
// Counter-scale for constant-size chrome (frame labels, the Interact
|
|
6654
|
+
// button). This has to be written on the SAME imperative tick as the
|
|
6655
|
+
// world transform: the React `chromeScale` it falls back to is only
|
|
6656
|
+
// reconciled by the debounced commitView, so during a gesture the labels
|
|
6657
|
+
// rode the world scale and then eased back — the "text/labels jump in
|
|
6658
|
+
// sizing" the canvas zoom was reported for.
|
|
6659
|
+
world.style.setProperty(
|
|
6660
|
+
CHROME_SCALE_CSS_VAR,
|
|
6661
|
+
String(nextScale > 0 ? 1 / nextScale : 1),
|
|
6662
|
+
);
|
|
6561
6663
|
}
|
|
6562
6664
|
const grid = pixelGridRef.current;
|
|
6563
6665
|
if (grid) {
|
|
@@ -6764,13 +6866,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6764
6866
|
|
|
6765
6867
|
if (gesture.mode === "zoom") {
|
|
6766
6868
|
const currentZoom = zoomRef.current;
|
|
6767
|
-
const zoomDeltaY = clamp(
|
|
6768
|
-
gesture.deltaY,
|
|
6769
|
-
-MAX_WHEEL_ZOOM_DELTA,
|
|
6770
|
-
MAX_WHEEL_ZOOM_DELTA,
|
|
6771
|
-
);
|
|
6772
6869
|
const nextZoom = clamp(
|
|
6773
|
-
currentZoom *
|
|
6870
|
+
currentZoom * resolveZoomFactor(gesture.deltaY, gesture.pinch),
|
|
6774
6871
|
MIN_ZOOM,
|
|
6775
6872
|
MAX_ZOOM,
|
|
6776
6873
|
);
|
|
@@ -6803,10 +6900,18 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6803
6900
|
const enqueueWheelGesture = useCallback(
|
|
6804
6901
|
(gesture: PendingWheelGesture) => {
|
|
6805
6902
|
const pending = pendingWheelGestureRef.current;
|
|
6806
|
-
|
|
6903
|
+
// Only accumulate like with like: a pinch delta and a wheel notch map to
|
|
6904
|
+
// zoom through different curves, so summing them would apply one curve to
|
|
6905
|
+
// the other's units.
|
|
6906
|
+
if (
|
|
6907
|
+
pending?.mode === "zoom" &&
|
|
6908
|
+
gesture.mode === "zoom" &&
|
|
6909
|
+
pending.pinch === gesture.pinch
|
|
6910
|
+
) {
|
|
6807
6911
|
pendingWheelGestureRef.current = {
|
|
6808
6912
|
mode: "zoom",
|
|
6809
6913
|
deltaY: pending.deltaY + gesture.deltaY,
|
|
6914
|
+
pinch: gesture.pinch,
|
|
6810
6915
|
cursor: gesture.cursor,
|
|
6811
6916
|
clientX: gesture.clientX,
|
|
6812
6917
|
clientY: gesture.clientY,
|
|
@@ -6852,14 +6957,13 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
6852
6957
|
// no forced style/layout read on the hot pan path.
|
|
6853
6958
|
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
6854
6959
|
if (!rect) return;
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
MAX_WHEEL_ZOOM_DELTA,
|
|
6859
|
-
);
|
|
6960
|
+
// Raw delta: the per-frame clamp lives on the zoom FACTOR now (see
|
|
6961
|
+
// resolveZoomFactor), so accumulation stays frame-rate independent.
|
|
6962
|
+
// Line/page delta modes are always discrete wheels, never a pinch.
|
|
6860
6963
|
enqueueWheelGesture({
|
|
6861
6964
|
mode: "zoom",
|
|
6862
|
-
deltaY:
|
|
6965
|
+
deltaY: delta.y,
|
|
6966
|
+
pinch: args.deltaMode === 0 && isPinchZoomDelta(args.deltaY),
|
|
6863
6967
|
cursor: {
|
|
6864
6968
|
x: args.clientX - rect.left,
|
|
6865
6969
|
y: args.clientY - rect.top,
|
|
@@ -7935,7 +8039,8 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
7935
8039
|
chromeScale={chromeScale}
|
|
7936
8040
|
chromeSettling={chromeSettling}
|
|
7937
8041
|
onPick={handleFrameClick}
|
|
7938
|
-
onEdit={
|
|
8042
|
+
onEdit={handleFrameInteract}
|
|
8043
|
+
onEnterFrame={handleFrameEnter}
|
|
7939
8044
|
onStartFrameDrag={beginFrameDrag}
|
|
7940
8045
|
onStartResize={beginResize}
|
|
7941
8046
|
onStartRotate={beginRotate}
|
|
@@ -9248,7 +9353,12 @@ interface ScreenProps {
|
|
|
9248
9353
|
* descriptor is retained for an evicted screen's revisit. */
|
|
9249
9354
|
cullTier: ScreenCullTier;
|
|
9250
9355
|
onPick: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
9356
|
+
/** Explicit Interact entry — the frame label's own button and the frame-label
|
|
9357
|
+
* row. Deliberately NOT the frame body's double-click: see onEnterFrame. */
|
|
9251
9358
|
onEdit: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
9359
|
+
/** Double-click on the frame body. Descends into the frame's layers instead
|
|
9360
|
+
* of entering Interact, matching Figma. */
|
|
9361
|
+
onEnterFrame: (id: string, e: React.MouseEvent<HTMLElement>) => void;
|
|
9252
9362
|
onStartFrameDrag: (id: string, e: React.MouseEvent) => void;
|
|
9253
9363
|
onStartResize: (
|
|
9254
9364
|
id: string,
|
|
@@ -9262,12 +9372,13 @@ interface ScreenProps {
|
|
|
9262
9372
|
e: React.MouseEvent<HTMLElement>,
|
|
9263
9373
|
) => void;
|
|
9264
9374
|
// Id-first (screenId, widthPx) shape, same as MultiScreenCanvas's own
|
|
9265
|
-
//
|
|
9266
|
-
//
|
|
9267
|
-
//
|
|
9268
|
-
//
|
|
9269
|
-
//
|
|
9270
|
-
|
|
9375
|
+
// onActiveBreakpointChange prop (PF18): Screen binds screen.id itself when
|
|
9376
|
+
// calling these, so the parent can pass the same stable function reference
|
|
9377
|
+
// for every screen instead of allocating a new per-screen closure on every
|
|
9378
|
+
// render, which defeated memo(Screen) for every screen every time.
|
|
9379
|
+
// onAddBreakpoint is design-scoped and takes no screen id at all — see its
|
|
9380
|
+
// doc on MultiScreenCanvasProps.
|
|
9381
|
+
onAddBreakpoint?: (widthPx: number) => void;
|
|
9271
9382
|
onActiveBreakpointChange?: (
|
|
9272
9383
|
screenId: string,
|
|
9273
9384
|
widthPx: number | undefined,
|
|
@@ -9309,6 +9420,7 @@ const Screen = memo(function Screen({
|
|
|
9309
9420
|
chromeSettling,
|
|
9310
9421
|
onPick,
|
|
9311
9422
|
onEdit,
|
|
9423
|
+
onEnterFrame,
|
|
9312
9424
|
onStartFrameDrag,
|
|
9313
9425
|
onStartResize,
|
|
9314
9426
|
onStartRotate,
|
|
@@ -9483,9 +9595,9 @@ const Screen = memo(function Screen({
|
|
|
9483
9595
|
style={{
|
|
9484
9596
|
width: labelInfoMaxWidth,
|
|
9485
9597
|
maxWidth: labelInfoMaxWidth,
|
|
9486
|
-
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
9598
|
+
transform: `translateY(-50%) scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
9487
9599
|
transformOrigin: "left center",
|
|
9488
|
-
transition: getChromeLabelTransition(
|
|
9600
|
+
transition: getChromeLabelTransition(),
|
|
9489
9601
|
}}
|
|
9490
9602
|
>
|
|
9491
9603
|
{/* B5-3: the leading dot/bullet before the screen label was pure
|
|
@@ -9555,9 +9667,9 @@ const Screen = memo(function Screen({
|
|
|
9555
9667
|
)}
|
|
9556
9668
|
style={{
|
|
9557
9669
|
maxWidth: fullViewMaxWidth,
|
|
9558
|
-
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
9670
|
+
transform: `translateY(-50%) scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
9559
9671
|
transformOrigin: "right center",
|
|
9560
|
-
transition: getChromeLabelTransition(
|
|
9672
|
+
transition: getChromeLabelTransition(),
|
|
9561
9673
|
}}
|
|
9562
9674
|
aria-label={frameActionLabel}
|
|
9563
9675
|
title={frameActionLabel}
|
|
@@ -9599,7 +9711,7 @@ const Screen = memo(function Screen({
|
|
|
9599
9711
|
}
|
|
9600
9712
|
e.preventDefault();
|
|
9601
9713
|
e.stopPropagation();
|
|
9602
|
-
|
|
9714
|
+
onEnterFrame(screen.id, e);
|
|
9603
9715
|
}}
|
|
9604
9716
|
onMouseDown={(e) => {
|
|
9605
9717
|
if (isInteractiveScreenContentTarget(e.target)) {
|
|
@@ -9818,11 +9930,7 @@ const Screen = memo(function Screen({
|
|
|
9818
9930
|
? (widthPx) => onActiveBreakpointChange(screen.id, widthPx)
|
|
9819
9931
|
: undefined
|
|
9820
9932
|
}
|
|
9821
|
-
onAddBreakpoint={
|
|
9822
|
-
onAddBreakpoint
|
|
9823
|
-
? (widthPx) => onAddBreakpoint(screen.id, widthPx)
|
|
9824
|
-
: undefined
|
|
9825
|
-
}
|
|
9933
|
+
onAddBreakpoint={onAddBreakpoint}
|
|
9826
9934
|
onRemoveBreakpoint={
|
|
9827
9935
|
onRemoveBreakpoint
|
|
9828
9936
|
? (widthPx) => onRemoveBreakpoint(screen.id, widthPx)
|
|
@@ -9893,6 +10001,7 @@ function areScreenPropsEqual(prev: ScreenProps, next: ScreenProps) {
|
|
|
9893
10001
|
prev.chromeSettling === next.chromeSettling &&
|
|
9894
10002
|
prev.onPick === next.onPick &&
|
|
9895
10003
|
prev.onEdit === next.onEdit &&
|
|
10004
|
+
prev.onEnterFrame === next.onEnterFrame &&
|
|
9896
10005
|
prev.onStartFrameDrag === next.onStartFrameDrag &&
|
|
9897
10006
|
prev.onStartResize === next.onStartResize &&
|
|
9898
10007
|
prev.onStartRotate === next.onStartRotate &&
|
|
@@ -9911,8 +10020,6 @@ function areScreenPropsEqual(prev: ScreenProps, next: ScreenProps) {
|
|
|
9911
10020
|
|
|
9912
10021
|
// ── Breakpoint preview row (§6.4) ────────────────────────────────────────────
|
|
9913
10022
|
|
|
9914
|
-
/** Gap between adjacent breakpoint frames in canvas pixels. */
|
|
9915
|
-
const BREAKPOINT_FRAME_GAP = 24;
|
|
9916
10023
|
const BREAKPOINT_LABEL_WITH_WIDTH_MIN_FRAME_WIDTH = 128;
|
|
9917
10024
|
|
|
9918
10025
|
/**
|
|
@@ -10156,9 +10263,9 @@ function BreakpointPreviewRow({
|
|
|
10156
10263
|
data-frame-label
|
|
10157
10264
|
className="absolute left-1 top-1/2 flex min-w-0 max-w-[calc(100%-28px)] items-center gap-1.5"
|
|
10158
10265
|
style={{
|
|
10159
|
-
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
10266
|
+
transform: `translateY(-50%) scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
10160
10267
|
transformOrigin: "left center",
|
|
10161
|
-
transition: getChromeLabelTransition(
|
|
10268
|
+
transition: getChromeLabelTransition(),
|
|
10162
10269
|
}}
|
|
10163
10270
|
>
|
|
10164
10271
|
<span
|
|
@@ -10200,7 +10307,7 @@ function BreakpointPreviewRow({
|
|
|
10200
10307
|
<div
|
|
10201
10308
|
className="absolute right-1 top-1/2 z-30"
|
|
10202
10309
|
style={{
|
|
10203
|
-
transform: `translateY(-50%) scale(${chromeScale})`,
|
|
10310
|
+
transform: `translateY(-50%) scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
10204
10311
|
transformOrigin: "right center",
|
|
10205
10312
|
}}
|
|
10206
10313
|
>
|
|
@@ -10302,7 +10409,7 @@ function BreakpointPreviewRow({
|
|
|
10302
10409
|
}}
|
|
10303
10410
|
onDoubleClick={(e) => {
|
|
10304
10411
|
// Item 8b — full view for THIS breakpoint: same double-click
|
|
10305
|
-
// gesture as a regular screen's card (
|
|
10412
|
+
// gesture as a regular screen's card (handleFrameInteract),
|
|
10306
10413
|
// but targeting the breakpoint's own width so the editor
|
|
10307
10414
|
// opens single-screen mode scoped to it instead of Base.
|
|
10308
10415
|
e.preventDefault();
|
|
@@ -10336,7 +10443,7 @@ function BreakpointPreviewRow({
|
|
|
10336
10443
|
data-frame-full-view
|
|
10337
10444
|
className="absolute right-1 top-1 z-30 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border border-border bg-background/95 text-muted-foreground opacity-0 shadow-sm transition-opacity hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/artboard:opacity-100"
|
|
10338
10445
|
style={{
|
|
10339
|
-
transform: `scale(${chromeScale})`,
|
|
10446
|
+
transform: `scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
10340
10447
|
transformOrigin: "right center",
|
|
10341
10448
|
}}
|
|
10342
10449
|
aria-label={frameActionLabel}
|
|
@@ -10472,10 +10579,17 @@ function BreakpointPreviewRow({
|
|
|
10472
10579
|
"hover:border-[var(--design-editor-accent-color)] hover:text-[var(--design-editor-accent-color)]",
|
|
10473
10580
|
)}
|
|
10474
10581
|
style={{
|
|
10475
|
-
transform: `scale(${chromeScale})`,
|
|
10582
|
+
transform: `scale(var(${CHROME_SCALE_CSS_VAR}, ${chromeScale}))`,
|
|
10476
10583
|
transformOrigin: "center",
|
|
10477
10584
|
}}
|
|
10478
|
-
|
|
10585
|
+
// Says "to all screens" because that is what it does: a design has
|
|
10586
|
+
// one breakpoint set, so this is not scoped to the frame it renders
|
|
10587
|
+
// beside. The old per-frame wording made every frame suddenly
|
|
10588
|
+
// sprouting the new width look like a bug.
|
|
10589
|
+
title={t("multiScreenCanvas.addBreakpointToAllScreens", {
|
|
10590
|
+
label: breakpointLabel(nextWidth),
|
|
10591
|
+
width: nextWidth,
|
|
10592
|
+
})}
|
|
10479
10593
|
onClick={(e) => {
|
|
10480
10594
|
e.stopPropagation();
|
|
10481
10595
|
onAddBreakpoint(nextWidth);
|
|
@@ -12177,7 +12177,11 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
12177
12177
|
typeof e.data.correlationId === "string" ? e.data.correlationId : "";
|
|
12178
12178
|
var textEditStatusNodeId: string =
|
|
12179
12179
|
typeof e.data.nodeId === "string" ? e.data.nodeId : "";
|
|
12180
|
-
|
|
12180
|
+
// "missing" (this document has no such node) stays distinct from a bare
|
|
12181
|
+
// `false` (node is here, just not being edited). The host's retry ladder
|
|
12182
|
+
// needs the difference: the first is a still-propagating insert or the
|
|
12183
|
+
// wrong iframe, the second means the user has not typed yet.
|
|
12184
|
+
var textEditStatus: "active" | "done" | "missing" | false = "missing";
|
|
12181
12185
|
if (textEditStatusNodeId) {
|
|
12182
12186
|
var escapedTextEditStatusNodeId = textEditStatusNodeId
|
|
12183
12187
|
.replace(/\\/g, "\\\\")
|
|
@@ -12201,6 +12205,8 @@ declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
|
12201
12205
|
(textEditStatusNode.textContent ?? "").trim().length > 0
|
|
12202
12206
|
) {
|
|
12203
12207
|
textEditStatus = "done";
|
|
12208
|
+
} else if (textEditStatusNode) {
|
|
12209
|
+
textEditStatus = false;
|
|
12204
12210
|
}
|
|
12205
12211
|
}
|
|
12206
12212
|
(window.parent as Window).postMessage(
|
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
PopoverContent,
|
|
29
29
|
PopoverTrigger,
|
|
30
30
|
} from "@/components/ui/popover";
|
|
31
|
+
import {
|
|
32
|
+
Tooltip,
|
|
33
|
+
TooltipContent,
|
|
34
|
+
TooltipTrigger,
|
|
35
|
+
} from "@/components/ui/tooltip";
|
|
31
36
|
import { cn } from "@/lib/utils";
|
|
32
37
|
|
|
33
38
|
import { ScrubInput } from "../inspector";
|
|
@@ -37,6 +42,7 @@ import {
|
|
|
37
42
|
type GlslShaderPanelContext,
|
|
38
43
|
} from "../inspector/GlslShaderPanel";
|
|
39
44
|
import type { ElementInfo } from "../types";
|
|
45
|
+
import { isTextElement } from "./element-classification";
|
|
40
46
|
import { elementStableKey } from "./element-identity";
|
|
41
47
|
import { FieldTrailer } from "./field-primitives";
|
|
42
48
|
import { splitCssLayers } from "./fill-gradient-helpers";
|
|
@@ -155,6 +161,26 @@ export function serializeShadowLayers(layers: ShadowLayer[]) {
|
|
|
155
161
|
.join(", ");
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/**
|
|
165
|
+
* `text-shadow` form of the same layers: offsets, blur, colour. CSS text-shadow
|
|
166
|
+
* has no spread and no inset, so emitting the box-shadow string would produce a
|
|
167
|
+
* declaration the browser drops entirely.
|
|
168
|
+
*/
|
|
169
|
+
export function serializeTextShadowLayers(layers: ShadowLayer[]) {
|
|
170
|
+
const visible = layers.filter((layer) => !layer.inset);
|
|
171
|
+
if (!visible.length) return "none";
|
|
172
|
+
return visible
|
|
173
|
+
.map((layer) =>
|
|
174
|
+
[
|
|
175
|
+
`${roundToOneDecimal(layer.x)}px`,
|
|
176
|
+
`${roundToOneDecimal(layer.y)}px`,
|
|
177
|
+
`${Math.max(0, roundToOneDecimal(layer.blur))}px`,
|
|
178
|
+
layer.color,
|
|
179
|
+
].join(" "),
|
|
180
|
+
)
|
|
181
|
+
.join(", ");
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
export function readBlurFilter(value: string | undefined): number {
|
|
159
185
|
const match = value?.match(/blur\((-?(?:\d+(?:\.\d+)?|\.\d+))px\)/);
|
|
160
186
|
return match ? Math.max(0, Number(match[1])) : 0;
|
|
@@ -479,13 +505,27 @@ export function EffectsProperties({
|
|
|
479
505
|
const effectStashKey = elementStableKey(element);
|
|
480
506
|
const layerBlurStashKey = `${effectStashKey}:filter:blur`;
|
|
481
507
|
const backdropBlurStashKey = `${effectStashKey}:backdrop-filter:blur`;
|
|
508
|
+
const shadowTargetsText = isTextElement(element);
|
|
509
|
+
// Read from the same property the writer targets, or the rows would show a
|
|
510
|
+
// box-shadow that is no longer what this element uses.
|
|
482
511
|
const shadowLayers = effectsAreMixed
|
|
483
512
|
? []
|
|
484
|
-
: parseShadowLayers(
|
|
513
|
+
: parseShadowLayers(
|
|
514
|
+
shadowTargetsText ? styles.textShadow : styles.boxShadow,
|
|
515
|
+
);
|
|
485
516
|
const setShadowLayers = (layers: ShadowLayer[], meta?: StyleChangeMeta) => {
|
|
486
517
|
setHiddenEffectStash((stash) =>
|
|
487
518
|
remapIndexedShadowStash(stash, effectStashKey, layers),
|
|
488
519
|
);
|
|
520
|
+
// A shadow on text belongs on the glyphs. box-shadow paints the element's
|
|
521
|
+
// BOX, which on a text node reads as a rectangle floating behind the words
|
|
522
|
+
// instead of a shadow on the letters.
|
|
523
|
+
if (shadowTargetsText) {
|
|
524
|
+
const textShadow = serializeTextShadowLayers(layers);
|
|
525
|
+
if (onStylesChange) onStylesChange({ textShadow }, meta);
|
|
526
|
+
else onStyleChange("textShadow", textShadow, meta);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
489
529
|
const boxShadow = serializeShadowLayers(layers);
|
|
490
530
|
if (onStylesChange) onStylesChange({ boxShadow }, meta);
|
|
491
531
|
else onStyleChange("boxShadow", boxShadow, meta);
|
|
@@ -551,17 +591,24 @@ export function EffectsProperties({
|
|
|
551
591
|
title={t("editPanel.sections.effects")}
|
|
552
592
|
actions={
|
|
553
593
|
<DropdownMenu>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
594
|
+
{/* Tooltip as well as aria-label: this was the one section-header +
|
|
595
|
+
that explained itself to screen readers only. */}
|
|
596
|
+
<Tooltip>
|
|
597
|
+
<TooltipTrigger asChild>
|
|
598
|
+
<DropdownMenuTrigger asChild>
|
|
599
|
+
<Button
|
|
600
|
+
type="button"
|
|
601
|
+
variant="ghost"
|
|
602
|
+
size="icon"
|
|
603
|
+
className="size-6 cursor-pointer rounded-md text-muted-foreground hover:text-foreground"
|
|
604
|
+
aria-label={t("editPanel.labels.addEffect")}
|
|
605
|
+
>
|
|
606
|
+
<IconPlus className="size-3.5" />
|
|
607
|
+
</Button>
|
|
608
|
+
</DropdownMenuTrigger>
|
|
609
|
+
</TooltipTrigger>
|
|
610
|
+
<TooltipContent>{t("editPanel.labels.addEffect")}</TooltipContent>
|
|
611
|
+
</Tooltip>
|
|
565
612
|
<DropdownMenuContent align="end" className="min-w-44">
|
|
566
613
|
<DropdownMenuItem
|
|
567
614
|
className="gap-2 !text-[11px]"
|
|
@@ -279,7 +279,7 @@ export function FillProperties({
|
|
|
279
279
|
<IconLayoutGrid className="size-3.5" />
|
|
280
280
|
</SectionIconButton>
|
|
281
281
|
<SectionIconButton
|
|
282
|
-
label={t("editPanel.labels.
|
|
282
|
+
label={t("editPanel.labels.addFill")}
|
|
283
283
|
onClick={() => {
|
|
284
284
|
if (fillIsMixed) {
|
|
285
285
|
commitStylePatch(
|
|
@@ -28,36 +28,41 @@ export function SectionIconButton({
|
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<Tooltip>
|
|
31
|
+
{/* The span carries the hover, not the Button: a disabled Button stops
|
|
32
|
+
pointer events, so a disabled control would silently lose the very
|
|
33
|
+
tooltip that explains why it is disabled. */}
|
|
31
34
|
<TooltipTrigger asChild>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
pointerActivatedRef.current
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
35
|
+
<span className="flex shrink-0">
|
|
36
|
+
<Button
|
|
37
|
+
type="button"
|
|
38
|
+
variant="ghost"
|
|
39
|
+
size="icon"
|
|
40
|
+
className={cn(
|
|
41
|
+
"size-6 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:text-foreground disabled:cursor-not-allowed",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
onPointerDown={(event) => {
|
|
46
|
+
if (!activateOnPointerDown || disabled || event.button !== 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
pointerActivatedRef.current = true;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
onClick?.();
|
|
53
|
+
}}
|
|
54
|
+
onClick={() => {
|
|
55
|
+
if (pointerActivatedRef.current) {
|
|
56
|
+
pointerActivatedRef.current = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
onClick?.();
|
|
60
|
+
}}
|
|
61
|
+
aria-label={label}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</Button>
|
|
65
|
+
</span>
|
|
61
66
|
</TooltipTrigger>
|
|
62
67
|
<TooltipContent>{label}</TooltipContent>
|
|
63
68
|
</Tooltip>
|
|
@@ -352,7 +352,7 @@ export function StrokeProperties({
|
|
|
352
352
|
title={t("editPanel.sections.stroke")}
|
|
353
353
|
actions={
|
|
354
354
|
<SectionIconButton
|
|
355
|
-
label={t("editPanel.labels.
|
|
355
|
+
label={t("editPanel.labels.addStroke")}
|
|
356
356
|
onClick={() => {
|
|
357
357
|
if (strokeIsMixed) {
|
|
358
358
|
commitStylePatch(
|
|
@@ -537,7 +537,7 @@ function TextStrokeProperties({
|
|
|
537
537
|
title={t("editPanel.sections.stroke")}
|
|
538
538
|
actions={
|
|
539
539
|
<SectionIconButton
|
|
540
|
-
label={t("editPanel.labels.
|
|
540
|
+
label={t("editPanel.labels.addStroke")}
|
|
541
541
|
onClick={() => {
|
|
542
542
|
// Kebab-case keys required: camelCase webkit props get mangled by
|
|
543
543
|
// normalizeStyleProperty (camel→kebab drops the leading dash) and
|