@leviosa-ai/detail-page-editor 0.15.0 → 0.15.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.
@@ -115,6 +115,14 @@ export function LeviosaCanvasWorkspace({
115
115
  const outerRef = useRef<HTMLDivElement>(null);
116
116
  const innerRef = useRef<HTMLDivElement>(null);
117
117
  const [viewport, setViewport] = useState({ width: 0, height: 0 });
118
+ const [panning, setPanning] = useState(false);
119
+ const pan = useRef<{
120
+ pointerId: number;
121
+ x: number;
122
+ y: number;
123
+ left: number;
124
+ top: number;
125
+ } | null>(null);
118
126
  // 배율은 **스토어**에 산다. 확대 버튼도, 여기 휠도 같은 자리를 만져야 한 쪽이
119
127
  // 다른 쪽을 되돌려 놓지 않는다.
120
128
  const scale = store.scale;
@@ -415,7 +423,10 @@ export function LeviosaCanvasWorkspace({
415
423
  const target = event.target as HTMLElement;
416
424
  if (
417
425
  target.closest("[data-lc-page]") ||
418
- target.closest("[data-dp-quicktoolbar]")
426
+ target.closest("[data-dp-quicktoolbar]") ||
427
+ target.closest(
428
+ "button, a, input, textarea, select, [contenteditable='true']",
429
+ )
419
430
  ) {
420
431
  return;
421
432
  }
@@ -424,21 +435,49 @@ export function LeviosaCanvasWorkspace({
424
435
  // 여기가 «저 벌을 보겠다»고 말하는 유일한 자리다.
425
436
  const key = target.closest<HTMLElement>("[data-lc-frame]")?.dataset
426
437
  .lcFrame;
427
- if (key === undefined) return;
428
- const first = store.pages.find((page) => frameOf(page) === key);
429
- if (first) store.selectPage(first.id);
438
+ if (key !== undefined) {
439
+ const first = store.pages.find((page) => frameOf(page) === key);
440
+ if (first) store.selectPage(first.id);
441
+ }
442
+ if (event.button !== 0) return;
443
+ pan.current = {
444
+ pointerId: event.pointerId,
445
+ x: event.clientX,
446
+ y: event.clientY,
447
+ left: event.currentTarget.scrollLeft,
448
+ top: event.currentTarget.scrollTop,
449
+ };
450
+ event.currentTarget.setPointerCapture?.(event.pointerId);
451
+ setPanning(true);
452
+ event.preventDefault();
453
+ }}
454
+ onPointerMove={(event) => {
455
+ const start = pan.current;
456
+ if (!start || start.pointerId !== event.pointerId) return;
457
+ event.currentTarget.scrollLeft = start.left + start.x - event.clientX;
458
+ event.currentTarget.scrollTop = start.top + start.y - event.clientY;
459
+ }}
460
+ onPointerUp={(event) => {
461
+ if (pan.current?.pointerId !== event.pointerId) return;
462
+ pan.current = null;
463
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
464
+ setPanning(false);
465
+ }}
466
+ onPointerCancel={(event) => {
467
+ if (pan.current?.pointerId !== event.pointerId) return;
468
+ pan.current = null;
469
+ setPanning(false);
430
470
  }}
431
471
  style={{
432
472
  position: "absolute",
433
473
  inset: 0,
434
- // 프레임이 하나뿐이면 예전 그대로다. 가운데 정렬은 내용이 넘칠 때
435
- // 왼쪽으로 스크롤을 못 하게 만드는 자리라, 열이 여럿일 때는 왼쪽에
436
- // 붙여 놓고 가로 스크롤을 연다.
437
- overflowX: frameCount > 1 ? "auto" : "hidden",
474
+ // 자동 여백으로 가운데를 잡으면 확대해 넘친 뒤에도 양쪽으로 스크롤할 수 있다.
475
+ overflowX: "auto",
438
476
  overflowY: "auto",
439
477
  display: "flex",
440
478
  flexDirection: "column",
441
- alignItems: frameCount > 1 ? "flex-start" : "center",
479
+ alignItems: "flex-start",
480
+ cursor: panning ? "grabbing" : "default",
442
481
  padding: `${frameCount > 1 ? gap + FRAME_HEAD : gap}px ${paddingX}px ${gap}px`,
443
482
  }}
444
483
  >
@@ -446,9 +485,9 @@ export function LeviosaCanvasWorkspace({
446
485
  store={store}
447
486
  scale={scale}
448
487
  gap={gap}
449
- // 열이 여럿이면 가운데 정렬을 정렬 속성이 아니라 자동 여백으로 준다 —
450
- // 축소해서 남는 자리가 생겨도 가운데를 지키고, 커져도 스크롤이 산다.
451
- center={frameCount > 1}
488
+ // 가운데 정렬은 자동 여백으로 준다 — 축소해서 남는 자리가 생겨도 가운데를
489
+ // 지키고, 커지면 여백이 0이 되어 좌우 스크롤이 열린다.
490
+ center
452
491
  // 손잡이는 판 상자 **안**에 산다. 밖에서 자리를 재어 띄우면 손이 다가가는
453
492
  // 동안 «판 밖»을 지나며 깜빡인다.
454
493
  renderPageChrome={
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviosa-ai/detail-page-editor",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Leviosa detail-page editor shell. Source-only: consumers compile it through Next transpilePackages. Every app coupling (sourcing server, brand store, cache keys, toast) arrives through the DetailPageHost interface, and every URL it fetches comes from configureDetailPageEditor.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",