@kubuild/editor 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -384,19 +384,55 @@ var useEditorStore = create((set, get) => ({
384
384
  setAiGenerationStatus: (status) => set({ aiGenerationStatus: status }),
385
385
  insertComponent: (type, registry, parentId, index) => {
386
386
  const state = get();
387
- const targetParentId = parentId ?? state.selectedNodeId ?? state.document.document.id;
388
- const parentNode = findNodeById(state.document.document, targetParentId);
387
+ let targetParentId = parentId ?? state.selectedNodeId ?? state.document.document.id;
388
+ let targetIndex = index;
389
+ let parentNode = findNodeById(state.document.document, targetParentId);
389
390
  if (!parentNode) {
390
- return {
391
- success: false,
392
- error: `Insertion target "${targetParentId}" was not found in the document.`
393
- };
391
+ targetParentId = state.document.document.id;
392
+ parentNode = state.document.document;
394
393
  }
395
394
  const definition = registry.get(type);
396
395
  if (!definition) {
397
396
  return { success: false, error: `Unknown component type "${type}".` };
398
397
  }
399
- const policy = registry.canInsertChild(parentNode.type, type);
398
+ let policy = registry.canInsertChild(parentNode.type, type);
399
+ if (!policy.valid && !parentId) {
400
+ const loc = findNodeLocation(state.document.document, targetParentId);
401
+ if (loc && loc.parent) {
402
+ const parentPolicy = registry.canInsertChild(loc.parent.type, type);
403
+ if (parentPolicy.valid) {
404
+ targetParentId = loc.parent.id;
405
+ targetIndex = loc.index + 1;
406
+ parentNode = loc.parent;
407
+ policy = parentPolicy;
408
+ }
409
+ }
410
+ if (!policy.valid && parentNode.type === "page" && type !== "section") {
411
+ const sections = parentNode.children?.filter((c) => c.type === "section") ?? [];
412
+ const lastSection = sections[sections.length - 1];
413
+ if (lastSection) {
414
+ const sectionPolicy = registry.canInsertChild(lastSection.type, type);
415
+ if (sectionPolicy.valid) {
416
+ targetParentId = lastSection.id;
417
+ targetIndex = lastSection.children?.length ?? 0;
418
+ parentNode = lastSection;
419
+ policy = sectionPolicy;
420
+ } else {
421
+ const containers = lastSection.children?.filter((c) => c.type === "container") ?? [];
422
+ const lastContainer = containers[containers.length - 1];
423
+ if (lastContainer) {
424
+ const containerPolicy = registry.canInsertChild(lastContainer.type, type);
425
+ if (containerPolicy.valid) {
426
+ targetParentId = lastContainer.id;
427
+ targetIndex = lastContainer.children?.length ?? 0;
428
+ parentNode = lastContainer;
429
+ policy = containerPolicy;
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ }
400
436
  if (!policy.valid) {
401
437
  return { success: false, error: policy.errors.join(" ") };
402
438
  }
@@ -424,7 +460,7 @@ var useEditorStore = create((set, get) => ({
424
460
  ...definition.defaultStyles ? { styles: deepClone(definition.defaultStyles) } : {},
425
461
  ...children ? { children } : {}
426
462
  };
427
- get().dispatch((doc) => insertNode(doc, { parentId: targetParentId, node, index }));
463
+ get().dispatch((doc) => insertNode(doc, { parentId: targetParentId, node, index: targetIndex }));
428
464
  if (!state.activeArtboardId && OVERLAY_COMPONENT_TYPES.includes(type)) {
429
465
  const detached = get().detachNodeToArtboard(nodeId, { name: definition.label });
430
466
  if (detached.success && detached.triggerId && detached.stubNodeId) {
@@ -526,14 +562,36 @@ var useEditorStore = create((set, get) => ({
526
562
  error: "Cannot move a node into itself or one of its own descendants."
527
563
  };
528
564
  }
529
- const policy = registry.canInsertChild(targetParent.type, sourceLocation.node.type);
530
- if (!policy.valid) {
531
- return { success: false, error: policy.errors.join(" ") };
565
+ if (registry) {
566
+ const policy = registry.canInsertChild(targetParent.type, sourceLocation.node.type);
567
+ if (!policy.valid) {
568
+ return { success: false, error: policy.errors.join(" ") };
569
+ }
532
570
  }
533
571
  const adjustedIndex = sourceLocation.parent.id === targetParentId && typeof index === "number" && index > sourceLocation.index ? index - 1 : index;
534
572
  get().dispatch((doc) => moveNode(doc, { nodeId, targetParentId, index: adjustedIndex }));
535
573
  return { success: true };
536
574
  },
575
+ moveComponentUp: (nodeId, registry) => {
576
+ const state = get();
577
+ const loc = findNodeLocation(state.document.document, nodeId);
578
+ if (!loc || !loc.parent || loc.index <= 0) {
579
+ return { success: false, error: "Cannot move up: already at the top." };
580
+ }
581
+ return get().moveComponent(nodeId, loc.parent.id, registry, loc.index - 1);
582
+ },
583
+ moveComponentDown: (nodeId, registry) => {
584
+ const state = get();
585
+ const loc = findNodeLocation(state.document.document, nodeId);
586
+ if (!loc || !loc.parent) {
587
+ return { success: false, error: "Node parent not found." };
588
+ }
589
+ const siblingCount = loc.parent.children?.length ?? 1;
590
+ if (loc.index >= siblingCount - 1) {
591
+ return { success: false, error: "Cannot move down: already at the bottom." };
592
+ }
593
+ return get().moveComponent(nodeId, loc.parent.id, registry, loc.index + 2);
594
+ },
537
595
  duplicateComponent: (nodeId, _registry) => {
538
596
  const state = get();
539
597
  if (nodeId === state.document.document.id) {
@@ -4523,7 +4581,7 @@ import {
4523
4581
  // src/components/canvas/floating-badges.tsx
4524
4582
  import { ARTBOARD_REFERENCE_NODE_TYPE } from "@kubuild/schema";
4525
4583
  import { findNodeById as findNodeById4, getParentNodeId as getParentNodeId2 } from "@kubuild/core";
4526
- import { ArrowUp, Move, Copy as Copy2, Trash2 as Trash24, ExternalLink } from "lucide-react";
4584
+ import { ArrowUp, ChevronUp as ChevronUp3, ChevronDown as ChevronDown3, Move, Copy as Copy2, Trash2 as Trash24, ExternalLink } from "lucide-react";
4527
4585
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4528
4586
  var FloatingActionBadges = ({
4529
4587
  selectedNodeId,
@@ -4536,6 +4594,8 @@ var FloatingActionBadges = ({
4536
4594
  selectNode,
4537
4595
  duplicateComponent,
4538
4596
  deleteComponent,
4597
+ moveComponentUp,
4598
+ moveComponentDown,
4539
4599
  detachNodeToArtboard,
4540
4600
  activateArtboard,
4541
4601
  activeArtboardId
@@ -4593,6 +4653,30 @@ var FloatingActionBadges = ({
4593
4653
  children: /* @__PURE__ */ jsx7(Move, { className: "w-3.5 h-3.5", "aria-hidden": "true" })
4594
4654
  }
4595
4655
  ),
4656
+ !isRoot && /* @__PURE__ */ jsx7(
4657
+ "button",
4658
+ {
4659
+ type: "button",
4660
+ "data-testid": "floating-badge-move-up",
4661
+ title: "Move Up",
4662
+ "aria-label": "Move Up",
4663
+ onClick: () => moveComponentUp(node.id, registry),
4664
+ className: "px-1.5 py-1 hover:bg-blue-500 active:bg-blue-700 transition flex items-center justify-center border-l border-blue-500/40 text-[11px]",
4665
+ children: /* @__PURE__ */ jsx7(ChevronUp3, { className: "w-3.5 h-3.5", "aria-hidden": "true" })
4666
+ }
4667
+ ),
4668
+ !isRoot && /* @__PURE__ */ jsx7(
4669
+ "button",
4670
+ {
4671
+ type: "button",
4672
+ "data-testid": "floating-badge-move-down",
4673
+ title: "Move Down",
4674
+ "aria-label": "Move Down",
4675
+ onClick: () => moveComponentDown(node.id, registry),
4676
+ className: "px-1.5 py-1 hover:bg-blue-500 active:bg-blue-700 transition flex items-center justify-center border-l border-blue-500/40 text-[11px]",
4677
+ children: /* @__PURE__ */ jsx7(ChevronDown3, { className: "w-3.5 h-3.5", "aria-hidden": "true" })
4678
+ }
4679
+ ),
4596
4680
  !isRoot && /* @__PURE__ */ jsx7(
4597
4681
  "button",
4598
4682
  {
@@ -5143,6 +5227,7 @@ var ResizeHandles = ({
5143
5227
  borderRadius: "1px",
5144
5228
  boxShadow: "0 1px 2px rgba(0, 0, 0, 0.15)",
5145
5229
  pointerEvents: "auto",
5230
+ touchAction: "none",
5146
5231
  cursor,
5147
5232
  zIndex: 51,
5148
5233
  boxSizing: "border-box",
@@ -6133,6 +6218,7 @@ function useCanvasPanZoom({
6133
6218
  const isSpacePressedRef = useRef5(isSpacePressed);
6134
6219
  isSpacePressedRef.current = isSpacePressed;
6135
6220
  const dragStartRef = useRef5(null);
6221
+ const activePointerIdRef = useRef5(null);
6136
6222
  useEffect7(() => {
6137
6223
  if (!enabled) return;
6138
6224
  const onKeyDown = (e) => {
@@ -6192,16 +6278,94 @@ function useCanvasPanZoom({
6192
6278
  container.addEventListener("wheel", onWheel, { passive: false });
6193
6279
  return () => container.removeEventListener("wheel", onWheel);
6194
6280
  }, [containerRef, enabled]);
6281
+ const rafPanRef = useRef5(null);
6282
+ const pendingPanRef = useRef5(null);
6283
+ useEffect7(() => {
6284
+ const container = containerRef.current;
6285
+ if (!container || !enabled) return;
6286
+ let touchStartDistance = 0;
6287
+ let touchStartZoom = 1;
6288
+ let touchStartPan = { x: 0, y: 0 };
6289
+ let touchStartMid = { x: 0, y: 0 };
6290
+ let isPinching = false;
6291
+ const onTouchStart = (e) => {
6292
+ if (e.touches.length === 2) {
6293
+ e.preventDefault();
6294
+ activePointerIdRef.current = null;
6295
+ dragStartRef.current = null;
6296
+ pendingPanRef.current = null;
6297
+ if (rafPanRef.current !== null) {
6298
+ cancelAnimationFrame(rafPanRef.current);
6299
+ rafPanRef.current = null;
6300
+ }
6301
+ const t1 = e.touches[0];
6302
+ const t2 = e.touches[1];
6303
+ touchStartDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
6304
+ touchStartZoom = zoomRef.current;
6305
+ touchStartPan = { ...panRef.current };
6306
+ touchStartMid = {
6307
+ x: (t1.clientX + t2.clientX) / 2,
6308
+ y: (t1.clientY + t2.clientY) / 2
6309
+ };
6310
+ isPinching = true;
6311
+ setIsPanning(true);
6312
+ }
6313
+ };
6314
+ const onTouchMove = (e) => {
6315
+ if (e.touches.length === 2 && isPinching && touchStartDistance > 0) {
6316
+ e.preventDefault();
6317
+ const t1 = e.touches[0];
6318
+ const t2 = e.touches[1];
6319
+ const currentDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
6320
+ const scale = currentDistance / touchStartDistance;
6321
+ const nextZoom = clampZoom(touchStartZoom * scale);
6322
+ const currentMidX = (t1.clientX + t2.clientX) / 2;
6323
+ const currentMidY = (t1.clientY + t2.clientY) / 2;
6324
+ const rect = container.getBoundingClientRect();
6325
+ const midContainerX = touchStartMid.x - rect.left;
6326
+ const midContainerY = touchStartMid.y - rect.top;
6327
+ const newPanX = midContainerX - (midContainerX - touchStartPan.x) * (nextZoom / touchStartZoom) + (currentMidX - touchStartMid.x);
6328
+ const newPanY = midContainerY - (midContainerY - touchStartPan.y) * (nextZoom / touchStartZoom) + (currentMidY - touchStartMid.y);
6329
+ if (rafPanRef.current === null) {
6330
+ rafPanRef.current = requestAnimationFrame(() => {
6331
+ setZoom(nextZoom);
6332
+ setPan({ x: Math.round(newPanX), y: Math.round(newPanY) });
6333
+ rafPanRef.current = null;
6334
+ });
6335
+ }
6336
+ }
6337
+ };
6338
+ const onTouchEnd = (e) => {
6339
+ if (isPinching && e.touches.length < 2) {
6340
+ isPinching = false;
6341
+ touchStartDistance = 0;
6342
+ setIsPanning(false);
6343
+ }
6344
+ };
6345
+ container.addEventListener("touchstart", onTouchStart, { passive: false });
6346
+ container.addEventListener("touchmove", onTouchMove, { passive: false });
6347
+ container.addEventListener("touchend", onTouchEnd, { passive: false });
6348
+ container.addEventListener("touchcancel", onTouchEnd, { passive: false });
6349
+ return () => {
6350
+ container.removeEventListener("touchstart", onTouchStart);
6351
+ container.removeEventListener("touchmove", onTouchMove);
6352
+ container.removeEventListener("touchend", onTouchEnd);
6353
+ container.removeEventListener("touchcancel", onTouchEnd);
6354
+ };
6355
+ }, [containerRef, enabled]);
6195
6356
  const handlePointerDown = useCallback5(
6196
- (e) => {
6357
+ (e, forcePan = false) => {
6197
6358
  if (!enabled) return;
6359
+ const isTouch = e.pointerType === "touch" || e.pointerType === "pen";
6360
+ if (isTouch && e.isPrimary === false) return;
6198
6361
  const isMiddleClick = e.button === 1;
6199
6362
  const isSpacePan = isSpacePressedRef.current && e.button === 0;
6200
- const isHandMode = toolModeRef.current === "hand" && e.button === 0;
6201
- if (isMiddleClick || isSpacePan || isHandMode) {
6363
+ const isHandMode = toolModeRef.current === "hand" && (e.button === 0 || isTouch);
6364
+ if (forcePan || isMiddleClick || isSpacePan || isHandMode) {
6202
6365
  e.preventDefault();
6203
6366
  e.stopPropagation();
6204
6367
  setIsPanning(true);
6368
+ activePointerIdRef.current = e.pointerId;
6205
6369
  dragStartRef.current = {
6206
6370
  startX: e.clientX,
6207
6371
  startY: e.clientY,
@@ -6215,14 +6379,33 @@ function useCanvasPanZoom({
6215
6379
  const handlePointerMove = useCallback5((e) => {
6216
6380
  const drag = dragStartRef.current;
6217
6381
  if (!drag) return;
6382
+ if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current) return;
6218
6383
  const deltaX = e.clientX - drag.startX;
6219
6384
  const deltaY = e.clientY - drag.startY;
6220
- setPan({
6385
+ pendingPanRef.current = {
6221
6386
  x: Math.round(drag.initialPanX + deltaX),
6222
6387
  y: Math.round(drag.initialPanY + deltaY)
6223
- });
6388
+ };
6389
+ if (rafPanRef.current === null) {
6390
+ rafPanRef.current = requestAnimationFrame(() => {
6391
+ if (pendingPanRef.current) {
6392
+ setPan(pendingPanRef.current);
6393
+ }
6394
+ rafPanRef.current = null;
6395
+ });
6396
+ }
6224
6397
  }, []);
6225
- const handlePointerUp = useCallback5(() => {
6398
+ const handlePointerUp = useCallback5((e) => {
6399
+ if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current) return;
6400
+ if (rafPanRef.current !== null) {
6401
+ cancelAnimationFrame(rafPanRef.current);
6402
+ rafPanRef.current = null;
6403
+ }
6404
+ if (pendingPanRef.current) {
6405
+ setPan(pendingPanRef.current);
6406
+ pendingPanRef.current = null;
6407
+ }
6408
+ activePointerIdRef.current = null;
6226
6409
  if (dragStartRef.current) {
6227
6410
  dragStartRef.current = null;
6228
6411
  setIsPanning(false);
@@ -6235,6 +6418,10 @@ function useCanvasPanZoom({
6235
6418
  return () => {
6236
6419
  window.removeEventListener("pointermove", handlePointerMove);
6237
6420
  window.removeEventListener("pointerup", handlePointerUp);
6421
+ if (rafPanRef.current !== null) {
6422
+ cancelAnimationFrame(rafPanRef.current);
6423
+ rafPanRef.current = null;
6424
+ }
6238
6425
  };
6239
6426
  }
6240
6427
  }, [isPanning, handlePointerMove, handlePointerUp]);
@@ -6539,6 +6726,7 @@ var MultiDevicePreview = ({
6539
6726
  context,
6540
6727
  viewport: device.id,
6541
6728
  mode: previewMode ? "runtime" : "editor",
6729
+ selectedNodeId,
6542
6730
  onNodeClick: (id) => {
6543
6731
  selectNode(id);
6544
6732
  setViewport(device.id);
@@ -7231,7 +7419,7 @@ var ViewportResizer = ({
7231
7419
  showPresets && /* @__PURE__ */ jsxs15(
7232
7420
  "div",
7233
7421
  {
7234
- className: "flex items-center justify-between gap-2 w-full px-1 py-1 mb-2 select-none text-xs",
7422
+ className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1 w-full px-1 py-1 mb-2 select-none text-xs",
7235
7423
  onPointerDown: onHeaderPointerDown,
7236
7424
  children: [
7237
7425
  /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2 min-w-0 cursor-grab active:cursor-grabbing", children: [
@@ -7431,42 +7619,34 @@ var EditorCanvas = ({
7431
7619
  activeArtboardId: propActiveArtboardId,
7432
7620
  className
7433
7621
  }) => {
7434
- const {
7435
- document: storeDoc,
7436
- selectedNodeId,
7437
- selectedNodeIds,
7438
- hoveredNodeId,
7439
- dragPayload,
7440
- selectNode,
7441
- selectMultipleNodes,
7442
- toggleNodeSelection,
7443
- wrapSelectedIntoFrame,
7444
- ungroupSelectedFrame,
7445
- hoverNode,
7446
- updateNodeProps,
7447
- setDragPayload,
7448
- insertComponent,
7449
- insertBlock,
7450
- moveComponent,
7451
- deleteComponent,
7452
- duplicateComponent,
7453
- copyNode,
7454
- pasteNode,
7455
- undo,
7456
- redo,
7457
- previewMode,
7458
- multiDeviceMode,
7459
- toggleMultiDeviceMode,
7460
- addActionLog,
7461
- setLiveFormState,
7462
- aiGenerationStatus: storeAiGenerationStatus,
7463
- componentArtboards: storeComponentArtboards,
7464
- activeArtboardId: storeActiveArtboardId,
7465
- activateArtboard,
7466
- removeComponentArtboard,
7467
- setComponentArtboardPosition,
7468
- setComponentArtboardWidth
7469
- } = useEditorStore();
7622
+ const storeDoc = useEditorStore((s) => s.document);
7623
+ const selectedNodeId = useEditorStore((s) => s.selectedNodeId);
7624
+ const selectedNodeIds = useEditorStore((s) => s.selectedNodeIds);
7625
+ const hoveredNodeId = useEditorStore((s) => s.hoveredNodeId);
7626
+ const dragPayload = useEditorStore((s) => s.dragPayload);
7627
+ const selectNode = useEditorStore((s) => s.selectNode);
7628
+ const selectMultipleNodes = useEditorStore((s) => s.selectMultipleNodes);
7629
+ const toggleNodeSelection = useEditorStore((s) => s.toggleNodeSelection);
7630
+ const hoverNode = useEditorStore((s) => s.hoverNode);
7631
+ const updateNodeProps = useEditorStore((s) => s.updateNodeProps);
7632
+ const setDragPayload = useEditorStore((s) => s.setDragPayload);
7633
+ const insertComponent = useEditorStore((s) => s.insertComponent);
7634
+ const insertBlock = useEditorStore((s) => s.insertBlock);
7635
+ const moveComponent = useEditorStore((s) => s.moveComponent);
7636
+ const deleteComponent = useEditorStore((s) => s.deleteComponent);
7637
+ const duplicateComponent = useEditorStore((s) => s.duplicateComponent);
7638
+ const previewMode = useEditorStore((s) => s.previewMode);
7639
+ const multiDeviceMode = useEditorStore((s) => s.multiDeviceMode);
7640
+ const toggleMultiDeviceMode = useEditorStore((s) => s.toggleMultiDeviceMode);
7641
+ const addActionLog = useEditorStore((s) => s.addActionLog);
7642
+ const setLiveFormState = useEditorStore((s) => s.setLiveFormState);
7643
+ const storeAiGenerationStatus = useEditorStore((s) => s.aiGenerationStatus);
7644
+ const storeComponentArtboards = useEditorStore((s) => s.componentArtboards);
7645
+ const storeActiveArtboardId = useEditorStore((s) => s.activeArtboardId);
7646
+ const activateArtboard = useEditorStore((s) => s.activateArtboard);
7647
+ const removeComponentArtboard = useEditorStore((s) => s.removeComponentArtboard);
7648
+ const setComponentArtboardPosition = useEditorStore((s) => s.setComponentArtboardPosition);
7649
+ const setComponentArtboardWidth = useEditorStore((s) => s.setComponentArtboardWidth);
7470
7650
  const document2 = propDoc ?? storeDoc;
7471
7651
  const aiGenerationStatus = propAiGenerationStatus ?? storeAiGenerationStatus;
7472
7652
  const componentArtboards = propComponentArtboards ?? storeComponentArtboards;
@@ -7727,6 +7907,14 @@ var EditorCanvas = ({
7727
7907
  const [draggingArtboardId, setDraggingArtboardId] = useState12(null);
7728
7908
  const [dragPosition, setDragPosition] = useState12(null);
7729
7909
  const marqueeDragRef = useRef7(null);
7910
+ const isTouchDevice = useMemo7(() => {
7911
+ if (typeof window === "undefined") return false;
7912
+ return window.matchMedia && window.matchMedia("(pointer: coarse)").matches || "ontouchstart" in window;
7913
+ }, []);
7914
+ const isSmallScreen = useMemo7(() => {
7915
+ if (typeof window === "undefined") return false;
7916
+ return window.innerWidth < 768;
7917
+ }, []);
7730
7918
  const {
7731
7919
  pan,
7732
7920
  setPan,
@@ -7821,14 +8009,23 @@ var EditorCanvas = ({
7821
8009
  setSelectedRects(multi);
7822
8010
  };
7823
8011
  recompute();
8012
+ let rafId = null;
8013
+ const throttledRecompute = () => {
8014
+ if (rafId !== null) return;
8015
+ rafId = requestAnimationFrame(() => {
8016
+ recompute();
8017
+ rafId = null;
8018
+ });
8019
+ };
7824
8020
  const container = containerRef.current;
7825
- window.addEventListener("resize", recompute);
7826
- window.addEventListener("scroll", recompute, true);
7827
- container?.addEventListener("input", recompute);
8021
+ window.addEventListener("resize", throttledRecompute);
8022
+ window.addEventListener("scroll", throttledRecompute, { passive: true, capture: true });
8023
+ container?.addEventListener("input", throttledRecompute);
7828
8024
  return () => {
7829
- window.removeEventListener("resize", recompute);
7830
- window.removeEventListener("scroll", recompute, true);
7831
- container?.removeEventListener("input", recompute);
8025
+ if (rafId !== null) cancelAnimationFrame(rafId);
8026
+ window.removeEventListener("resize", throttledRecompute);
8027
+ window.removeEventListener("scroll", throttledRecompute, true);
8028
+ container?.removeEventListener("input", throttledRecompute);
7832
8029
  };
7833
8030
  }, [activeDoc, selectedNodeId, selectedNodeIds, hoveredNodeId, viewport, zoom, pan, fluidWidth, effectiveActivePageId]);
7834
8031
  useEffect10(() => {
@@ -7923,6 +8120,7 @@ var EditorCanvas = ({
7923
8120
  };
7924
8121
  }, [registry]);
7925
8122
  const candidateRects = useMemo7(() => {
8123
+ if (isTouchDevice && isSmallScreen) return [];
7926
8124
  const layer = layerRef.current;
7927
8125
  if (!layer || !selectedNodeId) return [];
7928
8126
  const elements = layer.querySelectorAll("[data-kubuild-node]");
@@ -7942,12 +8140,16 @@ var EditorCanvas = ({
7942
8140
  }
7943
8141
  });
7944
8142
  return results;
7945
- }, [document2, selectedNodeId, zoom]);
8143
+ }, [document2, selectedNodeId, zoom, isTouchDevice, isSmallScreen]);
7946
8144
  const handleMouseOver = (e) => {
8145
+ if (isTouchDevice) return;
7947
8146
  const el = e.target.closest("[data-kubuild-node]");
7948
8147
  if (el) hoverNode(el.getAttribute("data-kubuild-node"));
7949
8148
  };
7950
- const handleMouseLeave = () => hoverNode(null);
8149
+ const handleMouseLeave = () => {
8150
+ if (isTouchDevice) return;
8151
+ hoverNode(null);
8152
+ };
7951
8153
  const handleCanvasPointerDown = (e) => {
7952
8154
  if (e.button === 1 || isSpacePressed || toolMode === "hand") {
7953
8155
  handlePanPointerDown(e);
@@ -7959,7 +8161,14 @@ var EditorCanvas = ({
7959
8161
  const isRootOrEmpty = !clickedNode || clickedNode.getAttribute("data-kubuild-node") === document2.document.id;
7960
8162
  const isDirectCanvasBg = target === containerRef.current || target === layerRef.current || target.getAttribute("data-testid") === "canvas-viewport-container" || target.getAttribute("data-testid") === "canvas-transform-layer";
7961
8163
  if (isDirectCanvasBg && !e.shiftKey) {
7962
- handlePanPointerDown(e);
8164
+ handlePanPointerDown(e, true);
8165
+ return;
8166
+ }
8167
+ if (e.pointerType === "touch" || e.pointerType === "pen") {
8168
+ if (isRootOrEmpty) {
8169
+ selectNode(null);
8170
+ handlePanPointerDown(e, true);
8171
+ }
7963
8172
  return;
7964
8173
  }
7965
8174
  if (isRootOrEmpty) {
@@ -8308,9 +8517,12 @@ var EditorCanvas = ({
8308
8517
  overflow: "hidden",
8309
8518
  cursor: cursorStyle,
8310
8519
  backgroundColor: "#f1f5f9",
8311
- backgroundImage: `radial-gradient(circle, #cbd5e1 ${Math.max(0.75, Math.min(2.5, 1.2 * zoom))}px, transparent ${Math.max(0.75, Math.min(2.5, 1.2 * zoom))}px)`,
8312
- backgroundSize: `${24 * zoom}px ${24 * zoom}px`,
8313
- backgroundPosition: `${pan.x}px ${pan.y}px`
8520
+ // App owns single-finger pan and two-finger pinch-zoom on this container itself,
8521
+ // so native browser pan/zoom must stay fully off to avoid the two fighting.
8522
+ touchAction: "none",
8523
+ backgroundImage: isSmallScreen && isTouchDevice ? void 0 : `radial-gradient(circle, #cbd5e1 ${Math.max(0.75, Math.min(2.5, 1.2 * zoom))}px, transparent ${Math.max(0.75, Math.min(2.5, 1.2 * zoom))}px)`,
8524
+ backgroundSize: isSmallScreen && isTouchDevice ? void 0 : `${24 * zoom}px ${24 * zoom}px`,
8525
+ backgroundPosition: isSmallScreen && isTouchDevice ? void 0 : `${pan.x}px ${pan.y}px`
8314
8526
  },
8315
8527
  onPointerDown: handleCanvasPointerDown,
8316
8528
  onPointerMove: handleCanvasPointerMove,
@@ -8389,6 +8601,7 @@ var EditorCanvas = ({
8389
8601
  context: contextForArtboard(pageItem),
8390
8602
  viewport: pageViewport,
8391
8603
  mode: previewMode ? "runtime" : "editor",
8604
+ selectedNodeId,
8392
8605
  onNodeClick: (id, e) => {
8393
8606
  if (!previewMode) {
8394
8607
  if (e?.shiftKey) {
@@ -8524,7 +8737,7 @@ var EditorCanvas = ({
8524
8737
  containerRef: activeArtboardRef
8525
8738
  }
8526
8739
  ),
8527
- !previewMode && !isMultiSelecting && selectedRect && selectedNodeId && selectedNodeId !== activeDoc.document.id && /* @__PURE__ */ jsx18(
8740
+ !previewMode && !isMultiSelecting && !(isTouchDevice && isSmallScreen) && selectedRect && selectedNodeId && selectedNodeId !== activeDoc.document.id && /* @__PURE__ */ jsx18(
8528
8741
  ResizeHandles,
8529
8742
  {
8530
8743
  selectedNodeId,
@@ -8534,7 +8747,7 @@ var EditorCanvas = ({
8534
8747
  onGuidesChange: setActiveGuides
8535
8748
  }
8536
8749
  ),
8537
- !previewMode && !isMultiSelecting && selectedRect && selectedNodeId && selectedNodeId !== activeDoc.document.id && /* @__PURE__ */ jsx18(
8750
+ !previewMode && !isMultiSelecting && !(isTouchDevice && isSmallScreen) && selectedRect && selectedNodeId && selectedNodeId !== activeDoc.document.id && /* @__PURE__ */ jsx18(
8538
8751
  SpacingSliders,
8539
8752
  {
8540
8753
  selectedNodeId,
@@ -17830,7 +18043,7 @@ var InspectorPanel = ({
17830
18043
  // src/components/panels/layers-panel.tsx
17831
18044
  import { useEffect as useEffect22, useState as useState30 } from "react";
17832
18045
  import { findNodeById as findNodeById10, findNodeLocation as findNodeLocation4, isDescendantOf as isDescendantOf3, getAncestorChain as getAncestorChain2 } from "@kubuild/core";
17833
- import { ChevronDown as ChevronDown3, ChevronRight as ChevronRight2 } from "lucide-react";
18046
+ import { ChevronDown as ChevronDown4, ChevronRight as ChevronRight2 } from "lucide-react";
17834
18047
  import { jsx as jsx41, jsxs as jsxs38 } from "react/jsx-runtime";
17835
18048
  var LayersPanel = ({ registry, className }) => {
17836
18049
  const { document: document2, selectedNodeId, hoveredNodeId, selectNode, hoverNode, moveComponent } = useEditorStore();
@@ -17962,7 +18175,7 @@ var LayersPanel = ({ registry, className }) => {
17962
18175
  },
17963
18176
  "aria-label": isExpanded ? "Collapse" : "Expand",
17964
18177
  className: "w-4 shrink-0 text-slate-400 hover:text-slate-700 flex items-center justify-center",
17965
- children: isExpanded ? /* @__PURE__ */ jsx41(ChevronDown3, { className: "w-3 h-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx41(ChevronRight2, { className: "w-3 h-3", "aria-hidden": "true" })
18178
+ children: isExpanded ? /* @__PURE__ */ jsx41(ChevronDown4, { className: "w-3 h-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx41(ChevronRight2, { className: "w-3 h-3", "aria-hidden": "true" })
17966
18179
  }
17967
18180
  ) : /* @__PURE__ */ jsx41("span", { className: "w-4 shrink-0" }),
17968
18181
  /* @__PURE__ */ jsx41("span", { className: "shrink-0 text-slate-400", children: /* @__PURE__ */ jsx41(ComponentIcon, { iconOrType: registry.get(node.type)?.icon ?? node.type, size: 13 }) }),
@@ -18012,7 +18225,12 @@ var CATEGORY_LABELS = {
18012
18225
  data: "Data",
18013
18226
  custom: "Custom"
18014
18227
  };
18015
- var ComponentPanel = ({ registry, config, className }) => {
18228
+ var ComponentPanel = ({
18229
+ registry,
18230
+ config,
18231
+ className,
18232
+ onItemInserted
18233
+ }) => {
18016
18234
  const insertComponent = useEditorStore((s) => s.insertComponent);
18017
18235
  const setDragPayload = useEditorStore((s) => s.setDragPayload);
18018
18236
  const [error, setError] = useState31(null);
@@ -18044,7 +18262,12 @@ var ComponentPanel = ({ registry, config, className }) => {
18044
18262
  }).filter((group) => group.items.length > 0);
18045
18263
  const handleInsert = (definition) => {
18046
18264
  const result = insertComponent(definition.type, registry);
18047
- setError(result.success ? null : result.error ?? `Could not insert "${definition.label}".`);
18265
+ if (result.success) {
18266
+ setError(null);
18267
+ onItemInserted?.();
18268
+ } else {
18269
+ setError(result.error ?? `Could not insert "${definition.label}".`);
18270
+ }
18048
18271
  };
18049
18272
  const handleDragStart = (e, definition) => {
18050
18273
  e.dataTransfer.effectAllowed = "copy";
@@ -18210,7 +18433,8 @@ var BlockThumbnail = ({ block }) => {
18210
18433
  var BlocksPanel = ({
18211
18434
  blocks = STARTER_BLOCKS3,
18212
18435
  className,
18213
- onInsertBlock
18436
+ onInsertBlock,
18437
+ onItemInserted
18214
18438
  }) => {
18215
18439
  const { document: document2, selectedNodeId, dispatch, selectNode } = useEditorStore();
18216
18440
  const [selectedCategory, setSelectedCategory] = useState32("all");
@@ -18230,6 +18454,7 @@ var BlocksPanel = ({
18230
18454
  const handleInsert = (block) => {
18231
18455
  if (onInsertBlock) {
18232
18456
  onInsertBlock(block);
18457
+ onItemInserted?.();
18233
18458
  return;
18234
18459
  }
18235
18460
  const existingIds = collectNodeIdSet3(document2.document);
@@ -18247,9 +18472,11 @@ var BlocksPanel = ({
18247
18472
  try {
18248
18473
  dispatch((doc) => insertNode3(doc, { parentId: targetParentId, node: nodeTree }));
18249
18474
  selectNode(nodeTree.id);
18475
+ onItemInserted?.();
18250
18476
  } catch {
18251
18477
  dispatch((doc) => insertNode3(doc, { parentId: document2.document.id, node: nodeTree }));
18252
18478
  selectNode(nodeTree.id);
18479
+ onItemInserted?.();
18253
18480
  }
18254
18481
  };
18255
18482
  const setDragPayload = useEditorStore((s) => s.setDragPayload);
@@ -18323,7 +18550,8 @@ var LeftSidebar = ({
18323
18550
  defaultTab: propDefaultTab,
18324
18551
  availableTabs: propAvailableTabs,
18325
18552
  config,
18326
- className
18553
+ className,
18554
+ onItemInserted
18327
18555
  }) => {
18328
18556
  const tabsList = config?.availableTabs ?? propAvailableTabs ?? ["components", "blocks", "layers"];
18329
18557
  const initialTabCandidate = config?.defaultTab ?? propDefaultTab ?? "components";
@@ -18398,8 +18626,8 @@ var LeftSidebar = ({
18398
18626
  }
18399
18627
  ),
18400
18628
  /* @__PURE__ */ jsxs41("div", { className: "flex-1 overflow-hidden min-h-0", children: [
18401
- activeTab === "components" && tabsList.includes("components") && /* @__PURE__ */ jsx44("div", { role: "tabpanel", id: "tabpanel-components", "aria-labelledby": "tab-components", className: "h-full", children: /* @__PURE__ */ jsx44(ComponentPanel, { registry, config }) }),
18402
- activeTab === "blocks" && tabsList.includes("blocks") && /* @__PURE__ */ jsx44("div", { role: "tabpanel", id: "tabpanel-blocks", "aria-labelledby": "tab-blocks", className: "h-full", children: /* @__PURE__ */ jsx44(BlocksPanel, { registry }) }),
18629
+ activeTab === "components" && tabsList.includes("components") && /* @__PURE__ */ jsx44("div", { role: "tabpanel", id: "tabpanel-components", "aria-labelledby": "tab-components", className: "h-full", children: /* @__PURE__ */ jsx44(ComponentPanel, { registry, config, onItemInserted }) }),
18630
+ activeTab === "blocks" && tabsList.includes("blocks") && /* @__PURE__ */ jsx44("div", { role: "tabpanel", id: "tabpanel-blocks", "aria-labelledby": "tab-blocks", className: "h-full", children: /* @__PURE__ */ jsx44(BlocksPanel, { registry, onItemInserted }) }),
18403
18631
  activeTab === "layers" && tabsList.includes("layers") && /* @__PURE__ */ jsx44("div", { role: "tabpanel", id: "tabpanel-layers", "aria-labelledby": "tab-layers", className: "h-full", children: /* @__PURE__ */ jsx44(LayersPanel, { registry }) })
18404
18632
  ] })
18405
18633
  ] });
@@ -18417,7 +18645,7 @@ import {
18417
18645
  Copy as Copy5,
18418
18646
  Check as Check7,
18419
18647
  ChevronRight as ChevronRight3,
18420
- ChevronDown as ChevronDown4,
18648
+ ChevronDown as ChevronDown5,
18421
18649
  RotateCcw as RotateCcw5,
18422
18650
  Sliders as Sliders3,
18423
18651
  Maximize2 as Maximize24,
@@ -18728,7 +18956,7 @@ var ActionDebuggerPanel = ({
18728
18956
  ] }),
18729
18957
  /* @__PURE__ */ jsxs42("div", { className: "flex items-center gap-1.5 shrink-0", children: [
18730
18958
  /* @__PURE__ */ jsx45("span", { className: "text-[10px] font-mono text-slate-500", children: log.timestamp.slice(11, 19) }),
18731
- isExpanded ? /* @__PURE__ */ jsx45(ChevronDown4, { className: "w-3 h-3 text-slate-400" }) : /* @__PURE__ */ jsx45(ChevronRight3, { className: "w-3 h-3 text-slate-400" })
18959
+ isExpanded ? /* @__PURE__ */ jsx45(ChevronDown5, { className: "w-3 h-3 text-slate-400" }) : /* @__PURE__ */ jsx45(ChevronRight3, { className: "w-3 h-3 text-slate-400" })
18732
18960
  ] })
18733
18961
  ]
18734
18962
  }
@@ -18771,13 +18999,17 @@ import {
18771
18999
  Undo2 as Undo22,
18772
19000
  Redo2 as Redo22,
18773
19001
  X as X9,
18774
- Boxes as Boxes2
19002
+ Boxes as Boxes2,
19003
+ ArrowUp as ArrowUp2,
19004
+ ArrowDown,
19005
+ Trash2 as Trash212
18775
19006
  } from "lucide-react";
18776
19007
  import { jsx as jsx46, jsxs as jsxs43 } from "react/jsx-runtime";
18777
19008
  var KubuildEditor = ({
18778
19009
  initialDocument,
18779
19010
  pages,
18780
19011
  activePageId,
19012
+ selectedNodeId: propSelectedNodeId,
18781
19013
  onActivePageChange,
18782
19014
  onPagesChange,
18783
19015
  registry = createDefaultComponentRegistry3(),
@@ -18789,27 +19021,29 @@ var KubuildEditor = ({
18789
19021
  ai,
18790
19022
  className
18791
19023
  }) => {
18792
- const {
18793
- document: document2,
18794
- setDocument,
18795
- setOnChangeHandler,
18796
- setVariableCatalog,
18797
- viewport,
18798
- setViewport,
18799
- selectedNodeId,
18800
- tableSpreadsheetMode,
18801
- setTableSpreadsheetMode,
18802
- aiChatMode,
18803
- setAiChatMode,
18804
- undo,
18805
- redo,
18806
- canUndo,
18807
- canRedo,
18808
- previewMode,
18809
- multiDeviceMode,
18810
- toggleMultiDeviceMode,
18811
- actionDebuggerOpen
18812
- } = useEditorStore();
19024
+ const document2 = useEditorStore((state) => state.document);
19025
+ const setDocument = useEditorStore((state) => state.setDocument);
19026
+ const setOnChangeHandler = useEditorStore((state) => state.setOnChangeHandler);
19027
+ const setVariableCatalog = useEditorStore((state) => state.setVariableCatalog);
19028
+ const viewport = useEditorStore((state) => state.viewport);
19029
+ const setViewport = useEditorStore((state) => state.setViewport);
19030
+ const storeSelectedNodeId = useEditorStore((state) => state.selectedNodeId);
19031
+ const selectedNodeId = propSelectedNodeId !== void 0 ? propSelectedNodeId : storeSelectedNodeId;
19032
+ const tableSpreadsheetMode = useEditorStore((state) => state.tableSpreadsheetMode);
19033
+ const setTableSpreadsheetMode = useEditorStore((state) => state.setTableSpreadsheetMode);
19034
+ const aiChatMode = useEditorStore((state) => state.aiChatMode);
19035
+ const setAiChatMode = useEditorStore((state) => state.setAiChatMode);
19036
+ const undo = useEditorStore((state) => state.undo);
19037
+ const redo = useEditorStore((state) => state.redo);
19038
+ const canUndo = useEditorStore((state) => state.canUndo);
19039
+ const canRedo = useEditorStore((state) => state.canRedo);
19040
+ const previewMode = useEditorStore((state) => state.previewMode);
19041
+ const multiDeviceMode = useEditorStore((state) => state.multiDeviceMode);
19042
+ const toggleMultiDeviceMode = useEditorStore((state) => state.toggleMultiDeviceMode);
19043
+ const actionDebuggerOpen = useEditorStore((state) => state.actionDebuggerOpen);
19044
+ const moveComponentUp = useEditorStore((state) => state.moveComponentUp);
19045
+ const moveComponentDown = useEditorStore((state) => state.moveComponentDown);
19046
+ const deleteComponent = useEditorStore((state) => state.deleteComponent);
18813
19047
  const lastLoadedDocRef = React39.useRef(void 0);
18814
19048
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState35(false);
18815
19049
  const [isMobileInspectorOpen, setIsMobileInspectorOpen] = useState35(false);
@@ -18986,7 +19220,14 @@ var KubuildEditor = ({
18986
19220
  }
18987
19221
  )
18988
19222
  ] }),
18989
- /* @__PURE__ */ jsx46("div", { className: "flex-1 overflow-hidden min-h-0", children: /* @__PURE__ */ jsx46(LeftSidebar, { registry, config: resolvedConfig.sidebar }) })
19223
+ /* @__PURE__ */ jsx46("div", { className: "flex-1 overflow-hidden min-h-0", children: /* @__PURE__ */ jsx46(
19224
+ LeftSidebar,
19225
+ {
19226
+ registry,
19227
+ config: resolvedConfig.sidebar,
19228
+ onItemInserted: () => setIsMobileSidebarOpen(false)
19229
+ }
19230
+ ) })
18990
19231
  ] })
18991
19232
  ] }),
18992
19233
  isMobileInspectorOpen && resolvedConfig.inspector.enabled && /* @__PURE__ */ jsxs43("div", { className: "fixed inset-0 z-50 flex justify-end lg:hidden animate-in fade-in duration-200", children: [
@@ -19129,22 +19370,61 @@ var KubuildEditor = ({
19129
19370
  className: "flex-1 min-h-0"
19130
19371
  }
19131
19372
  ),
19132
- selectedNodeId && !isMobileInspectorOpen && resolvedConfig.inspector.enabled && /* @__PURE__ */ jsx46("div", { className: "lg:hidden fixed bottom-14 left-1/2 -translate-x-1/2 z-30 animate-in fade-in slide-in-from-bottom-2", children: /* @__PURE__ */ jsxs43(
19133
- "button",
19373
+ selectedNodeId && !isMobileInspectorOpen && resolvedConfig.inspector.enabled && /* @__PURE__ */ jsxs43(
19374
+ "div",
19134
19375
  {
19135
- type: "button",
19136
- onClick: () => setIsMobileInspectorOpen(true),
19137
- className: "flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold px-4 py-2 rounded-full shadow-xl border border-blue-400/40 active:scale-95 transition",
19376
+ "data-testid": "mobile-selected-node-actions",
19377
+ className: "lg:hidden fixed bottom-14 left-1/2 -translate-x-1/2 z-30 animate-in fade-in slide-in-from-bottom-2 flex items-center bg-slate-900/90 text-white rounded-full shadow-2xl backdrop-blur-md border border-slate-700/60 p-1 gap-1",
19138
19378
  children: [
19139
- /* @__PURE__ */ jsx46(Sliders4, { className: "w-3.5 h-3.5" }),
19140
- /* @__PURE__ */ jsxs43("span", { children: [
19141
- "Edit Element (#",
19142
- selectedNodeId,
19143
- ")"
19144
- ] })
19379
+ /* @__PURE__ */ jsx46(
19380
+ "button",
19381
+ {
19382
+ type: "button",
19383
+ onClick: () => moveComponentUp(selectedNodeId, registry),
19384
+ title: "Move Up",
19385
+ "aria-label": "Move Up",
19386
+ className: "p-1.5 rounded-full hover:bg-slate-800 active:bg-slate-700 text-slate-200 hover:text-white transition cursor-pointer",
19387
+ children: /* @__PURE__ */ jsx46(ArrowUp2, { className: "w-3.5 h-3.5" })
19388
+ }
19389
+ ),
19390
+ /* @__PURE__ */ jsx46(
19391
+ "button",
19392
+ {
19393
+ type: "button",
19394
+ onClick: () => moveComponentDown(selectedNodeId, registry),
19395
+ title: "Move Down",
19396
+ "aria-label": "Move Down",
19397
+ className: "p-1.5 rounded-full hover:bg-slate-800 active:bg-slate-700 text-slate-200 hover:text-white transition cursor-pointer",
19398
+ children: /* @__PURE__ */ jsx46(ArrowDown, { className: "w-3.5 h-3.5" })
19399
+ }
19400
+ ),
19401
+ /* @__PURE__ */ jsx46("div", { className: "w-px h-3.5 bg-slate-700 mx-0.5" }),
19402
+ /* @__PURE__ */ jsxs43(
19403
+ "button",
19404
+ {
19405
+ type: "button",
19406
+ onClick: () => setIsMobileInspectorOpen(true),
19407
+ className: "flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-[11px] font-semibold px-2.5 py-1 rounded-full shadow-md active:scale-95 transition cursor-pointer",
19408
+ children: [
19409
+ /* @__PURE__ */ jsx46(Sliders4, { className: "w-3 h-3" }),
19410
+ /* @__PURE__ */ jsx46("span", { children: `Edit (#${selectedNodeId})` })
19411
+ ]
19412
+ }
19413
+ ),
19414
+ /* @__PURE__ */ jsx46(
19415
+ "button",
19416
+ {
19417
+ type: "button",
19418
+ onClick: () => deleteComponent(selectedNodeId),
19419
+ title: "Delete",
19420
+ "aria-label": "Delete",
19421
+ className: "p-1.5 rounded-full hover:bg-red-900/40 text-red-400 hover:text-red-300 transition active:scale-95 cursor-pointer",
19422
+ children: /* @__PURE__ */ jsx46(Trash212, { className: "w-3.5 h-3.5" })
19423
+ }
19424
+ )
19145
19425
  ]
19146
19426
  }
19147
- ) }),
19427
+ ),
19148
19428
  resolvedConfig.canvas.showBreadcrumbs && /* @__PURE__ */ jsx46(HierarchyBreadcrumbs, { registry }),
19149
19429
  previewMode && actionDebuggerOpen && /* @__PURE__ */ jsx46("div", { className: "fixed bottom-4 right-4 z-50 animate-in fade-in slide-in-from-bottom-3 duration-200", children: /* @__PURE__ */ jsx46(ActionDebuggerPanel, {}) })
19150
19430
  ] }),