@leviosa-ai/detail-page-editor 0.11.1 → 0.11.2

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.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useSyncExternalStore } from "react";
3
+ import { useRef, useState, useSyncExternalStore } from "react";
4
4
  import { observer } from "./canvas-observer";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { Wand2 } from "lucide-react";
@@ -17,9 +17,19 @@ import {
17
17
  * 예전에는 세로 띠에 AI·위·아래·복제·추가·삭제가 줄줄이 붙어 있었다. 화면을 가리는
18
18
  * 데 비해 쓰는 것은 하나뿐이었고, 나머지 넷은 페이지를 다루는 일이라 페이지 목록과
19
19
  * 오른쪽 페이지 패널로 옮겼다.
20
+ *
21
+ * 붙박이가 아니라 끌어서 옮길 수 있다. 옮긴 자리는 편집기가 열려 있는 동안 유지된다
22
+ * (화면을 바꿀 때마다 다시 붙는 위치로 튀면 옮긴 의미가 없다).
20
23
  */
21
24
 
22
25
  type PageLike = { id: string };
26
+ type Offset = { x: number; y: number };
27
+
28
+ /** 이 편집기 세션 동안 기억한다 — 화면을 바꿔도 버튼이 제자리에 있게. */
29
+ let keptOffset: Offset = { x: 0, y: 0 };
30
+
31
+ /** 이만큼도 못 움직였으면 끈 것이 아니라 누른 것이다. */
32
+ const DRAG_SLOP = 4;
23
33
 
24
34
  export const DetailPagePageToolbar = observer(function DetailPagePageToolbar({
25
35
  store,
@@ -36,6 +46,30 @@ export const DetailPagePageToolbar = observer(function DetailPagePageToolbar({
36
46
  () => false,
37
47
  );
38
48
  const p = page as PageLike;
49
+ const [offset, setOffset] = useState<Offset>(keptOffset);
50
+ const drag = useRef<{ x: number; y: number; from: Offset } | null>(null);
51
+ const dragged = useRef(false);
52
+
53
+ const move = (event: React.PointerEvent<HTMLButtonElement>) => {
54
+ const start = drag.current;
55
+ if (!start) return;
56
+ const dx = event.clientX - start.x;
57
+ const dy = event.clientY - start.y;
58
+ if (Math.abs(dx) > DRAG_SLOP || Math.abs(dy) > DRAG_SLOP)
59
+ dragged.current = true;
60
+ if (!dragged.current) return;
61
+ const rect = event.currentTarget.getBoundingClientRect();
62
+ // 화면 밖으로 던져 놓으면 다시 잡을 수가 없다 — 창 안으로 묶는다.
63
+ const next = { x: start.from.x + dx, y: start.from.y + dy };
64
+ const left = rect.left - offset.x + next.x;
65
+ const top = rect.top - offset.y + next.y;
66
+ const maxX = window.innerWidth - rect.width - 8;
67
+ const maxY = window.innerHeight - rect.height - 8;
68
+ next.x += Math.min(0, maxX - left) + Math.max(0, 8 - left);
69
+ next.y += Math.min(0, maxY - top) + Math.max(0, 8 - top);
70
+ keptOffset = next;
71
+ setOffset(next);
72
+ };
39
73
 
40
74
  // 다시 만들기를 못 꽂은 화면에서는 띠 자체가 없다 — 버튼이 하나뿐이라 그것이
41
75
  // 빠지면 빈 껍데기만 남는다.
@@ -50,11 +84,25 @@ export const DetailPagePageToolbar = observer(function DetailPagePageToolbar({
50
84
  aria-label={t("detailPage.pageToolbar.reauthor", {
51
85
  defaultValue: "이 화면 다시 만들기",
52
86
  })}
87
+ onPointerDown={(e) => {
88
+ if (e.button !== 0) return;
89
+ dragged.current = false;
90
+ drag.current = { x: e.clientX, y: e.clientY, from: offset };
91
+ e.currentTarget.setPointerCapture?.(e.pointerId);
92
+ }}
93
+ onPointerMove={move}
94
+ onPointerUp={(e) => {
95
+ drag.current = null;
96
+ e.currentTarget.releasePointerCapture?.(e.pointerId);
97
+ }}
53
98
  onClick={(e) => {
54
99
  e.stopPropagation();
100
+ // 끌어다 놓은 것이 다시 만들기로 이어지면 크레딧이 그냥 나간다.
101
+ if (dragged.current) return;
55
102
  requestSectionReauthor(p.id);
56
103
  }}
57
- className="flex h-10 w-10 items-center justify-center rounded-full border border-dpe-ink-200 bg-dpe-surface/95 shadow-md backdrop-blur-sm transition-colors hover:bg-dpe-ink-100"
104
+ style={{ transform: `translate(${offset.x}px, ${offset.y}px)` }}
105
+ className="flex h-10 w-10 cursor-grab touch-none items-center justify-center rounded-full border border-dpe-ink-200 bg-dpe-surface/95 shadow-md backdrop-blur-sm transition-colors hover:bg-dpe-ink-100 active:cursor-grabbing"
58
106
  >
59
107
  <Wand2 size={17} className="text-dpe-ai-alt" />
60
108
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviosa-ai/detail-page-editor",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
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",