@leviosa-ai/detail-page-editor 0.7.3 → 0.7.4

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.
@@ -15,7 +15,11 @@ import { useTranslation } from "react-i18next";
15
15
  import { observer } from "./canvas-observer";
16
16
  import { CANVAS_MENU_ICONS } from "./canvas-context-menu";
17
17
  import { ImageCropOverlay, type CropElement } from "./image-crop-overlay";
18
- import { SelectionQuickToolbar, type QuickToolbarItem } from "./selection-quick-toolbar";
18
+ import {
19
+ SelectionQuickToolbar,
20
+ useQuickPopoverPlacement,
21
+ type QuickToolbarItem,
22
+ } from "./selection-quick-toolbar";
19
23
  import { selectedElementsDeep, type SelectableElement } from "./detail-page-selection";
20
24
  import { useEditorAi } from "./editor-ai-context";
21
25
  import {
@@ -39,7 +43,13 @@ type ToolStore = CanvasMenuStore & {
39
43
  getElementById?: (id: string) => SelectableElement | undefined;
40
44
  };
41
45
 
42
- /** 창 하나. 띠 바로 아래에 붙고, 안에 들어가는 것은 우측 패널이 쓰던 부품 그대로다. */
46
+ /**
47
+ * 창 하나. 안에 들어가는 것은 우측 패널이 쓰던 부품 그대로다.
48
+ *
49
+ * 붙는 자리는 띠가 정한다(`useQuickPopoverPlacement`). 아래가 좁으면 위로 뒤집고,
50
+ * 높이는 그 자리에 실제로 남은 만큼으로 잘린다 — 넘치는 만큼은 스스로 스크롤한다.
51
+ * 60vh·520px 상한은 그대로 두되, 남은 자리가 더 좁으면 그쪽이 이긴다.
52
+ */
43
53
  function Popover({
44
54
  children,
45
55
  width = 360,
@@ -47,11 +57,16 @@ function Popover({
47
57
  children: React.ReactNode;
48
58
  width?: number;
49
59
  }) {
60
+ const { side, maxHeight } = useQuickPopoverPlacement();
50
61
  return (
51
62
  <div
52
63
  data-dp-quick-popover=""
53
- style={{ width, maxHeight: "min(60vh, 520px)" }}
54
- className="mt-1.5 overflow-y-auto rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface shadow-lg"
64
+ data-dp-quick-popover-side={side}
65
+ style={{ width, maxHeight: `min(60vh, 520px, ${maxHeight}px)` }}
66
+ className={[
67
+ "overflow-y-auto rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface shadow-lg",
68
+ side === "above" ? "absolute bottom-full left-0 mb-1.5" : "mt-1.5",
69
+ ].join(" ")}
55
70
  >
56
71
  {children}
57
72
  </div>
@@ -13,7 +13,9 @@
13
13
  */
14
14
 
15
15
  import {
16
+ createContext,
16
17
  useCallback,
18
+ useContext,
17
19
  useEffect,
18
20
  useLayoutEffect,
19
21
  useRef,
@@ -42,6 +44,10 @@ const GAP = 10;
42
44
  const EDGE = 8;
43
45
  /** 띠의 대략 높이. 실측 전 첫 프레임에만 쓴다. */
44
46
  const BAR_HEIGHT = 40;
47
+ /** 띠와 그 아래(또는 위) 창 사이의 여백(px). 클래스의 `mt-1.5`/`mb-1.5`와 같은 값이다. */
48
+ const POPOVER_GAP = 6;
49
+ /** 창이 이보다 낮아지면 접힌 것과 다름없다 — 스스로 스크롤하게 두고 이 높이는 지킨다. */
50
+ const POPOVER_MIN = 200;
45
51
 
46
52
  /**
47
53
  * 띠의 자리. 위가 모자라면 아래로 뒤집고, 좌우는 작업 영역 안으로 민다.
@@ -63,6 +69,40 @@ export function toolbarPosition(
63
69
  };
64
70
  }
65
71
 
72
+ /**
73
+ * 창이 띠의 위에 설지 아래에 설지, 그리고 얼마나 높을 수 있는지.
74
+ *
75
+ * 자리 계산이 재는 것은 **띠**뿐이라(`size`가 `barRef` 것이다) 창은 지금까지 아무에게도
76
+ * 안 물어보고 띠 아래로 흘렀다. 상세페이지 사진은 대개 한 섹션을 통째로 덮어서 띠가
77
+ * 작업 영역 맨 아래에 붙는데, 그러면 창은 통째로 화면 밖에 그려진다 — 열려 있고,
78
+ * 접근성 트리에도 있고, 눈에는 없다. 그 자리에서는 위로 뒤집는다.
79
+ */
80
+ export function popoverPlacement(
81
+ barTop: number,
82
+ barHeight: number,
83
+ hostHeight: number,
84
+ ): { side: "above" | "below"; maxHeight: number } {
85
+ const below = hostHeight - (barTop + barHeight) - POPOVER_GAP - EDGE;
86
+ const above = barTop - POPOVER_GAP - EDGE;
87
+ const side = below >= above ? "below" : "above";
88
+ return {
89
+ side,
90
+ maxHeight: Math.max(POPOVER_MIN, side === "below" ? below : above),
91
+ };
92
+ }
93
+
94
+ export type QuickPopoverPlacement = ReturnType<typeof popoverPlacement>;
95
+
96
+ const QuickPopoverContext = createContext<QuickPopoverPlacement>({
97
+ side: "below",
98
+ maxHeight: 520,
99
+ });
100
+
101
+ /** 창을 그리는 쪽이 자기 자리를 집는 곳. 띠 밖에서 부르면 기본값(아래)이다. */
102
+ export function useQuickPopoverPlacement(): QuickPopoverPlacement {
103
+ return useContext(QuickPopoverContext);
104
+ }
105
+
66
106
  export function SelectionQuickToolbar({
67
107
  els,
68
108
  items,
@@ -179,6 +219,7 @@ export function SelectionQuickToolbar({
179
219
  { width: host.clientWidth, height: host.clientHeight },
180
220
  size,
181
221
  );
222
+ const placement = popoverPlacement(top, size.height, host.clientHeight);
182
223
 
183
224
  return (
184
225
  <div
@@ -217,7 +258,9 @@ export function SelectionQuickToolbar({
217
258
  </div>
218
259
  ))}
219
260
  </div>
220
- {children}
261
+ <QuickPopoverContext.Provider value={placement}>
262
+ {children}
263
+ </QuickPopoverContext.Provider>
221
264
  </div>
222
265
  );
223
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviosa-ai/detail-page-editor",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
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",