@leviosa-ai/detail-page-editor 0.1.0
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/components/cardnews/color-input.tsx +380 -0
- package/components/detail-page/ai-generate-panel.tsx +763 -0
- package/components/detail-page/annotation-canvas.tsx +860 -0
- package/components/detail-page/annotation-dialog.tsx +397 -0
- package/components/detail-page/bubble-tail-overlay.tsx +209 -0
- package/components/detail-page/canvas-context-menu.tsx +234 -0
- package/components/detail-page/canvas-observer.tsx +68 -0
- package/components/detail-page/canvas-overlay-host.tsx +44 -0
- package/components/detail-page/canvas-store-facade.ts +51 -0
- package/components/detail-page/chart-inspector.tsx +531 -0
- package/components/detail-page/design-reference-picker.tsx +383 -0
- package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
- package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
- package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
- package/components/detail-page/detail-page-charts-panel.tsx +104 -0
- package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
- package/components/detail-page/detail-page-download-dialog.tsx +524 -0
- package/components/detail-page/detail-page-editor.tsx +974 -0
- package/components/detail-page/detail-page-elements-panel.tsx +117 -0
- package/components/detail-page/detail-page-font-picker.tsx +231 -0
- package/components/detail-page/detail-page-history-buttons.tsx +87 -0
- package/components/detail-page/detail-page-host-context.tsx +465 -0
- package/components/detail-page/detail-page-icons-panel.tsx +251 -0
- package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
- package/components/detail-page/detail-page-layers-panel.tsx +618 -0
- package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
- package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
- package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
- package/components/detail-page/detail-page-pages-panel.tsx +299 -0
- package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
- package/components/detail-page/detail-page-photos-panel.tsx +257 -0
- package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
- package/components/detail-page/detail-page-qr-panel.tsx +137 -0
- package/components/detail-page/detail-page-references-panel.tsx +166 -0
- package/components/detail-page/detail-page-selection.ts +44 -0
- package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
- package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
- package/components/detail-page/detail-page-tables-panel.tsx +106 -0
- package/components/detail-page/detail-page-text-panel.tsx +179 -0
- package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
- package/components/detail-page/edit-quota-ui.tsx +150 -0
- package/components/detail-page/editor-history.ts +56 -0
- package/components/detail-page/editor-hotkeys.tsx +165 -0
- package/components/detail-page/element-recents-strip.tsx +105 -0
- package/components/detail-page/element-rects.ts +82 -0
- package/components/detail-page/fill-control.tsx +261 -0
- package/components/detail-page/find-replace-panel.tsx +252 -0
- package/components/detail-page/generation-progress-fill.tsx +57 -0
- package/components/detail-page/gif-animator.tsx +184 -0
- package/components/detail-page/gif-effect-picker.tsx +136 -0
- package/components/detail-page/group-drill-in.tsx +219 -0
- package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
- package/components/detail-page/hover-highlight-overlay.tsx +111 -0
- package/components/detail-page/hovered-layer.ts +34 -0
- package/components/detail-page/inspector-controls.tsx +175 -0
- package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
- package/components/detail-page/nested-selection-overlay.tsx +364 -0
- package/components/detail-page/panel-search-input.tsx +49 -0
- package/components/detail-page/prompt-edit-panel.tsx +146 -0
- package/components/detail-page/section-height-handle.tsx +276 -0
- package/components/detail-page/section-reauthor-controller.tsx +210 -0
- package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
- package/components/detail-page/spec-resize-absorber.tsx +149 -0
- package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
- package/components/detail-page/table-canvas-overlay.tsx +707 -0
- package/components/detail-page/table-inspector.tsx +586 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/command.tsx +175 -0
- package/components/ui/dialog.tsx +167 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/select.tsx +190 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/tooltip.tsx +57 -0
- package/config/detail-page-bundle-font-tags.json +39 -0
- package/config/detail-page-fonts.json +373 -0
- package/index.ts +24 -0
- package/lib/cardnews/font-loader.ts +306 -0
- package/lib/detail-page/animation-sniff.ts +101 -0
- package/lib/detail-page/asset-bytes-url.ts +70 -0
- package/lib/detail-page/basic-shapes.ts +603 -0
- package/lib/detail-page/canvas-menu.ts +182 -0
- package/lib/detail-page/chart/defaults.ts +283 -0
- package/lib/detail-page/chart/format.ts +66 -0
- package/lib/detail-page/chart/normalize.ts +149 -0
- package/lib/detail-page/chart/palette.ts +85 -0
- package/lib/detail-page/chart/paste.ts +82 -0
- package/lib/detail-page/chart/preview.ts +78 -0
- package/lib/detail-page/chart/render.ts +188 -0
- package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
- package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
- package/lib/detail-page/chart/renderers/donut.ts +186 -0
- package/lib/detail-page/chart/renderers/gauge.ts +96 -0
- package/lib/detail-page/chart/renderers/line.ts +143 -0
- package/lib/detail-page/chart/renderers/shared.ts +263 -0
- package/lib/detail-page/chart/renderers/stack.ts +121 -0
- package/lib/detail-page/chart/sync.ts +92 -0
- package/lib/detail-page/chart/types.ts +145 -0
- package/lib/detail-page/css-color.ts +34 -0
- package/lib/detail-page/data-gif-payload.ts +167 -0
- package/lib/detail-page/design-reference.ts +244 -0
- package/lib/detail-page/distribute.ts +89 -0
- package/lib/detail-page/editor-routes.ts +10 -0
- package/lib/detail-page/element-recents.ts +167 -0
- package/lib/detail-page/find-replace.ts +120 -0
- package/lib/detail-page/format-painter.ts +159 -0
- package/lib/detail-page/gif-frames.ts +216 -0
- package/lib/detail-page/gif-timeline.ts +65 -0
- package/lib/detail-page/group-action.ts +62 -0
- package/lib/detail-page/hangul-roman.ts +58 -0
- package/lib/detail-page/icon-curated.ts +290 -0
- package/lib/detail-page/icon-fuzzy.ts +250 -0
- package/lib/detail-page/icon-keywords.generated.ts +3766 -0
- package/lib/detail-page/icon-keywords.ko.ts +524 -0
- package/lib/detail-page/icon-search.ts +257 -0
- package/lib/detail-page/icons.ts +67 -0
- package/lib/detail-page/image-credit.ts +121 -0
- package/lib/detail-page/image-data-uri.ts +29 -0
- package/lib/detail-page/insert-image.ts +75 -0
- package/lib/detail-page/insert-shape.ts +49 -0
- package/lib/detail-page/korean-dictionary.ts +187 -0
- package/lib/detail-page/layer-move.ts +257 -0
- package/lib/detail-page/new-product-draft-store.ts +105 -0
- package/lib/detail-page/new-product-draft.ts +283 -0
- package/lib/detail-page/personal-images-refresh.ts +30 -0
- package/lib/detail-page/product-source.ts +587 -0
- package/lib/detail-page/qr-code.ts +214 -0
- package/lib/detail-page/reference-image.ts +103 -0
- package/lib/detail-page/reference-upload.ts +181 -0
- package/lib/detail-page/replace-page.ts +35 -0
- package/lib/detail-page/replace-with-gif.ts +251 -0
- package/lib/detail-page/section-height.ts +131 -0
- package/lib/detail-page/section-reauthor-bus.ts +61 -0
- package/lib/detail-page/shape-to-image.ts +219 -0
- package/lib/detail-page/spec-group/clipboard.ts +35 -0
- package/lib/detail-page/spec-group/sync.ts +377 -0
- package/lib/detail-page/spec-group/text-metrics.ts +48 -0
- package/lib/detail-page/stock-photos.ts +88 -0
- package/lib/detail-page/svg-colors.ts +74 -0
- package/lib/detail-page/table/defaults.ts +344 -0
- package/lib/detail-page/table/edit.ts +312 -0
- package/lib/detail-page/table/harvest.ts +103 -0
- package/lib/detail-page/table/normalize.ts +125 -0
- package/lib/detail-page/table/paste.ts +49 -0
- package/lib/detail-page/table/preview.ts +66 -0
- package/lib/detail-page/table/render.ts +388 -0
- package/lib/detail-page/table/sync.ts +107 -0
- package/lib/detail-page/table/types.ts +102 -0
- package/lib/detail-page/text-gif-layout.ts +245 -0
- package/lib/detail-page/text-gif-payload.ts +67 -0
- package/lib/detail-page/text-presets.ts +230 -0
- package/lib/detail-page/use-fake-progress.ts +67 -0
- package/lib/detail-page/z-order.ts +82 -0
- package/lib/detail-page-canvas/bubble-path.ts +190 -0
- package/lib/detail-page-canvas/canvas-key.ts +22 -0
- package/lib/detail-page-canvas/clip-rect.ts +65 -0
- package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
- package/lib/detail-page-canvas/editor-fonts.ts +161 -0
- package/lib/detail-page-canvas/export/ai.ts +413 -0
- package/lib/detail-page-canvas/export/color.ts +35 -0
- package/lib/detail-page-canvas/export/document-model.ts +78 -0
- package/lib/detail-page-canvas/export/export-files.ts +290 -0
- package/lib/detail-page-canvas/export/font-subset.ts +264 -0
- package/lib/detail-page-canvas/export/gif-export.ts +310 -0
- package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
- package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
- package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
- package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
- package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
- package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
- package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
- package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
- package/lib/detail-page-canvas/export/psd.ts +354 -0
- package/lib/detail-page-canvas/export/raster.ts +255 -0
- package/lib/detail-page-canvas/export/svg.ts +479 -0
- package/lib/detail-page-canvas/export/text-layout.ts +142 -0
- package/lib/detail-page-canvas/font-catalog.ts +177 -0
- package/lib/detail-page-canvas/font-tags.ts +47 -0
- package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
- package/lib/detail-page-canvas/konva-fallback.ts +477 -0
- package/lib/detail-page-canvas/text-background-plan.ts +124 -0
- package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
- package/lib/utils.ts +10 -0
- package/package.json +68 -0
- package/types/commerce.ts +171 -0
- package/types/detail-page-api.ts +200 -0
- package/types/detail-page-canvas.ts +20 -0
- package/types/fontkit.d.ts +42 -0
- package/types/gifenc.d.ts +51 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState, type RefObject } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { observer } from "./canvas-observer";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
7
|
+
import {
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronUp,
|
|
10
|
+
ChevronsDown,
|
|
11
|
+
ChevronsUp,
|
|
12
|
+
Copy,
|
|
13
|
+
Group,
|
|
14
|
+
Lock,
|
|
15
|
+
Paintbrush,
|
|
16
|
+
PaintBucket,
|
|
17
|
+
Trash2,
|
|
18
|
+
Ungroup,
|
|
19
|
+
Unlock,
|
|
20
|
+
type LucideIcon,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
|
|
23
|
+
import { frontmostPath, type DrillElement } from "./group-drill-in";
|
|
24
|
+
import { konvaClientRect } from "./element-rects";
|
|
25
|
+
import {
|
|
26
|
+
canvasMenuItems,
|
|
27
|
+
runCanvasMenuAction,
|
|
28
|
+
type CanvasMenuAction,
|
|
29
|
+
type CanvasMenuStore,
|
|
30
|
+
} from "../../lib/detail-page/canvas-menu";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 캔버스 우클릭 메뉴.
|
|
34
|
+
*
|
|
35
|
+
* 스톡 편집기의 ``<Page>``는 ``contextmenu``를 안 단다 — 스톡 ``<Workspace>``가 달던 것이고
|
|
36
|
+
* 우리는 그걸 갈아치웠다(``stacked-canvas-workspace.tsx``). 그래서 워크스페이스
|
|
37
|
+
* 컨테이너에 우리가 직접 단다.
|
|
38
|
+
*
|
|
39
|
+
* 어느 요소를 대상으로 삼는지는 **스톡 편집기의 좌클릭과 같은 규칙**을 따른다: 커서 아래
|
|
40
|
+
* 최상위 요소. 다만 이미 그 안쪽(드릴인한 자식)이 선택돼 있으면 선택을 안 건드린다 —
|
|
41
|
+
* 그룹 안 도형을 골라 놓고 우클릭했는데 그룹으로 되돌아가면 메뉴가 남의 것을 만진다.
|
|
42
|
+
*
|
|
43
|
+
* 히트테스트는 ``GroupDrillIn``의 ``frontmostPath``를 그대로 쓴다. 스톡 편집기가 Konva
|
|
44
|
+
* 그룹에 요소 id를 안 박기 때문에 잎(leaf)부터 찾아 올라가야 한다.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
type MenuStore = CanvasMenuStore & {
|
|
48
|
+
pages?: Array<{ id: string; children?: DrillElement[] }>;
|
|
49
|
+
selectElements?: (ids: string[]) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ICONS: Record<CanvasMenuAction, LucideIcon> = {
|
|
53
|
+
duplicate: Copy,
|
|
54
|
+
lock: Lock,
|
|
55
|
+
unlock: Unlock,
|
|
56
|
+
delete: Trash2,
|
|
57
|
+
copyFormat: Paintbrush,
|
|
58
|
+
pasteFormat: PaintBucket,
|
|
59
|
+
front: ChevronsUp,
|
|
60
|
+
forward: ChevronUp,
|
|
61
|
+
backward: ChevronDown,
|
|
62
|
+
back: ChevronsDown,
|
|
63
|
+
group: Group,
|
|
64
|
+
ungroup: Ungroup,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const MENU_WIDTH = 200;
|
|
68
|
+
/** 화면 밖으로 안 나가게 미는 여백. */
|
|
69
|
+
const EDGE = 8;
|
|
70
|
+
|
|
71
|
+
/** 커서 자리에서 시작하되, 뷰포트를 넘으면 안쪽으로 당긴다. */
|
|
72
|
+
export function menuPosition(
|
|
73
|
+
point: { x: number; y: number },
|
|
74
|
+
viewport: { width: number; height: number },
|
|
75
|
+
size: { width: number; height: number },
|
|
76
|
+
): { left: number; top: number } {
|
|
77
|
+
const left = Math.max(
|
|
78
|
+
EDGE,
|
|
79
|
+
Math.min(point.x, viewport.width - size.width - EDGE),
|
|
80
|
+
);
|
|
81
|
+
const top = Math.max(
|
|
82
|
+
EDGE,
|
|
83
|
+
Math.min(point.y, viewport.height - size.height - EDGE),
|
|
84
|
+
);
|
|
85
|
+
return { left, top };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 우클릭이 가리키는 요소 id. 선택을 안 바꿔도 되면 null.
|
|
90
|
+
*
|
|
91
|
+
* 커서 아래 아무것도 없으면 ``{ hit: false }`` — 메뉴를 안 연다.
|
|
92
|
+
*/
|
|
93
|
+
export function contextTarget(
|
|
94
|
+
pages: ReadonlyArray<{ children?: DrillElement[] }>,
|
|
95
|
+
rectOf: (id: string) => { left: number; top: number; right: number; bottom: number } | null,
|
|
96
|
+
point: { x: number; y: number },
|
|
97
|
+
selectedIds: ReadonlyArray<string>,
|
|
98
|
+
): { hit: boolean; select: string | null } {
|
|
99
|
+
for (const page of pages) {
|
|
100
|
+
const path = frontmostPath(page.children ?? [], rectOf, point);
|
|
101
|
+
if (!path) continue;
|
|
102
|
+
const top = path[0];
|
|
103
|
+
// 선택이 이미 이 최상위 요소 안에 있으면 그대로 둔다(드릴인·다중선택 보존).
|
|
104
|
+
const inside = new Set<string>();
|
|
105
|
+
const collect = (el: DrillElement) => {
|
|
106
|
+
inside.add(el.id);
|
|
107
|
+
for (const c of el.children ?? []) collect(c);
|
|
108
|
+
};
|
|
109
|
+
collect(top);
|
|
110
|
+
if (selectedIds.some((id) => inside.has(id))) return { hit: true, select: null };
|
|
111
|
+
return { hit: true, select: top.id };
|
|
112
|
+
}
|
|
113
|
+
return { hit: false, select: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const CanvasContextMenu = observer(function CanvasContextMenu({
|
|
117
|
+
store,
|
|
118
|
+
containerRef,
|
|
119
|
+
}: {
|
|
120
|
+
store: unknown;
|
|
121
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
122
|
+
}) {
|
|
123
|
+
const { t } = useTranslation("branding");
|
|
124
|
+
const s = store as MenuStore;
|
|
125
|
+
const [at, setAt] = useState<{ x: number; y: number } | null>(null);
|
|
126
|
+
|
|
127
|
+
const close = useCallback(() => setAt(null), []);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const host = containerRef.current;
|
|
131
|
+
if (!host) return;
|
|
132
|
+
|
|
133
|
+
const onContextMenu = (e: MouseEvent) => {
|
|
134
|
+
const point = { x: e.clientX, y: e.clientY };
|
|
135
|
+
const target = contextTarget(
|
|
136
|
+
s.pages ?? [],
|
|
137
|
+
konvaClientRect,
|
|
138
|
+
point,
|
|
139
|
+
s.selectedElementsIds ?? [],
|
|
140
|
+
);
|
|
141
|
+
if (!target.hit) {
|
|
142
|
+
// 빈 캔버스: 브라우저 기본 메뉴에 맡긴다(새로고침·검사 등이 여전히 쓸모 있다).
|
|
143
|
+
setAt(null);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
if (target.select) s.selectElements?.([target.select]);
|
|
148
|
+
setAt(point);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
host.addEventListener("contextmenu", onContextMenu);
|
|
152
|
+
return () => host.removeEventListener("contextmenu", onContextMenu);
|
|
153
|
+
}, [s, containerRef]);
|
|
154
|
+
|
|
155
|
+
// 스크롤·줌·리사이즈로 캔버스가 움직이면 메뉴가 엉뚱한 자리를 가리킨다 — 닫는다.
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!at) return;
|
|
158
|
+
const onKey = (e: KeyboardEvent) => {
|
|
159
|
+
if (e.key === "Escape") close();
|
|
160
|
+
};
|
|
161
|
+
window.addEventListener("keydown", onKey);
|
|
162
|
+
window.addEventListener("resize", close);
|
|
163
|
+
window.addEventListener("wheel", close, { passive: true });
|
|
164
|
+
return () => {
|
|
165
|
+
window.removeEventListener("keydown", onKey);
|
|
166
|
+
window.removeEventListener("resize", close);
|
|
167
|
+
window.removeEventListener("wheel", close);
|
|
168
|
+
};
|
|
169
|
+
}, [at, close]);
|
|
170
|
+
|
|
171
|
+
const items = at ? canvasMenuItems(s) : [];
|
|
172
|
+
if (!at || items.length === 0) return null;
|
|
173
|
+
|
|
174
|
+
// 항목 높이 30 + 구분선 9 + 상하 여백 8. 실측 대신 계산으로 충분하다(살짝 어긋나도
|
|
175
|
+
// 화면 안이면 그만).
|
|
176
|
+
const height =
|
|
177
|
+
items.length * 30 + items.filter((i) => i.separated).length * 9 + 8;
|
|
178
|
+
const { left, top } = menuPosition(
|
|
179
|
+
at,
|
|
180
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
181
|
+
{ width: MENU_WIDTH, height },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 워크스페이스 **바깥**(body)에 그린다. 안에 두면 배경막을 누르는 pointerdown이
|
|
185
|
+
// GroupDrillIn의 캡처 리스너에 먼저 걸려, 메뉴를 닫으려는 클릭이 그룹 안 도형을
|
|
186
|
+
// 골라 버린다. 어차피 fixed 배치라 DOM 위치는 상관없다.
|
|
187
|
+
return createPortal(
|
|
188
|
+
<>
|
|
189
|
+
{/* 바깥 아무 데나 누르면 닫힌다. 우클릭도 잡아야 메뉴가 겹쳐 뜨지 않는다. */}
|
|
190
|
+
<div
|
|
191
|
+
data-dp-menu-backdrop
|
|
192
|
+
className="fixed inset-0 z-[70]"
|
|
193
|
+
onPointerDown={close}
|
|
194
|
+
onContextMenu={(e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
close();
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
<div
|
|
200
|
+
role="menu"
|
|
201
|
+
data-dp-canvas-menu
|
|
202
|
+
style={{ left, top, width: MENU_WIDTH }}
|
|
203
|
+
className="fixed z-[71] rounded-lg border border-neutral-200 bg-white py-1 shadow-lg"
|
|
204
|
+
>
|
|
205
|
+
{items.map((item) => {
|
|
206
|
+
const Icon = ICONS[item.action];
|
|
207
|
+
const label = t(`detailPage.canvasMenu.${item.action}`);
|
|
208
|
+
return (
|
|
209
|
+
<div key={item.action}>
|
|
210
|
+
{item.separated ? (
|
|
211
|
+
<div className="my-1 border-t border-neutral-100" />
|
|
212
|
+
) : null}
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
role="menuitem"
|
|
216
|
+
data-dp-menu-action={item.action}
|
|
217
|
+
disabled={item.disabled}
|
|
218
|
+
onClick={() => {
|
|
219
|
+
runCanvasMenuAction(s, item.action);
|
|
220
|
+
close();
|
|
221
|
+
}}
|
|
222
|
+
className="flex h-[30px] w-full items-center gap-2.5 px-3 text-left text-[13px] text-neutral-700 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
|
|
223
|
+
>
|
|
224
|
+
<Icon size={14} className="shrink-0 text-neutral-500" />
|
|
225
|
+
{label}
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
})}
|
|
230
|
+
</div>
|
|
231
|
+
</>,
|
|
232
|
+
document.body,
|
|
233
|
+
);
|
|
234
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 편집기 컴포넌트가 스토어 변경에 다시 그려지게 하는 자리 (G7-b).
|
|
5
|
+
*
|
|
6
|
+
* 편집기의 패널·오버레이는 거의 다 이 `observer`로 싸여 있다. 이름은 mobx에서 왔지만
|
|
7
|
+
* 이제 mobx가 아니다 — 우리 엔진 스토어는 리스너 + `version` 숫자이므로(하드룰: 새
|
|
8
|
+
* 런타임 의존성 0) 구독해서 버전을 읽으면 그만이다. 스톡 편집기가 사라지면서 관측할 mobx
|
|
9
|
+
* 값도 같이 사라졌고, `mobx-react-lite`는 그때 함께 걷어냈다. 남은 것은 그것이 얹어
|
|
10
|
+
* 주던 `memo` 한 겹이라 여기서 직접 씌운다.
|
|
11
|
+
*
|
|
12
|
+
* **한 겹 안에서 구독과 렌더를 같이 한다.** 감싼 컴포넌트를 자식 엘리먼트로 그리면
|
|
13
|
+
* (`<Component {...props}/>`) 구독한 쪽과 읽는 쪽이 갈라진다. 그래서 자식으로 두지 않고
|
|
14
|
+
* **그 자리에서 함수로 부른다**. 훅 목록이 [우리 것, …컴포넌트 것]으로 매번 같은
|
|
15
|
+
* 순서라 규칙에도 맞다.
|
|
16
|
+
*
|
|
17
|
+
* 밖에서 신호를 주는 방법(props에 버전을 얹기)은 안 쓴다 — `memo`를 뚫으려고 컴포넌트가
|
|
18
|
+
* 모르는 prop을 끼워 넣게 되고, 그게 DOM 속성으로 새면 조용한 경고가 된다.
|
|
19
|
+
*
|
|
20
|
+
* 왜 얼굴(`canvas-store-facade`)을 새로 만들어 내려보내지 않는가 — 그 방법도 되지만
|
|
21
|
+
* 스토어의 정체성이 매번 바뀌어서, `[store]`를 의존성으로 둔 effect가 글자 한 자마다
|
|
22
|
+
* 다시 붙는다(리스너 재등록, 타이머 재시작). 스토어는 그대로 두고 신호만 보낸다.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
createContext,
|
|
27
|
+
memo,
|
|
28
|
+
useContext,
|
|
29
|
+
useSyncExternalStore,
|
|
30
|
+
type ComponentType,
|
|
31
|
+
type ReactNode,
|
|
32
|
+
} from "react";
|
|
33
|
+
|
|
34
|
+
import type { CanvasStore } from "@leviosa-ai/canvas/store";
|
|
35
|
+
|
|
36
|
+
/** 이 편집기가 그리고 있는 스토어. 하네스처럼 안 꽂는 자리도 있어 `null`을 받는다. */
|
|
37
|
+
export const CanvasStoreContext = createContext<CanvasStore | null>(null);
|
|
38
|
+
|
|
39
|
+
/** 스토어가 없을 때 쓰는 붙박이 구독 — 매번 새 함수를 만들면 재구독이 돈다. */
|
|
40
|
+
const NO_SUBSCRIBE = () => () => {};
|
|
41
|
+
|
|
42
|
+
export function useCanvasStoreVersion(): number {
|
|
43
|
+
const store = useContext(CanvasStoreContext);
|
|
44
|
+
return useSyncExternalStore(
|
|
45
|
+
store?.subscribe ?? NO_SUBSCRIBE,
|
|
46
|
+
() => store?.version ?? 0,
|
|
47
|
+
() => 0,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 함수 컴포넌트만 받는다(`memo`·`forwardRef`로 이미 싸인 것은 그 자리에서 부를 수
|
|
53
|
+
* 없다). 편집기의 `observer(function …)` 자리들이 전부 여기 해당하고, 아닌 것을
|
|
54
|
+
* 넘기면 타입 단계에서 걸린다 — 조용히 다른 길로 새는 것보다 낫다.
|
|
55
|
+
*/
|
|
56
|
+
export function observer<P extends object>(
|
|
57
|
+
Component: (props: P) => ReactNode,
|
|
58
|
+
): ComponentType<P> {
|
|
59
|
+
function Reactive(props: P) {
|
|
60
|
+
useCanvasStoreVersion();
|
|
61
|
+
// 자식 엘리먼트로 두지 않고 그 자리에서 부른다(위 주석).
|
|
62
|
+
return Component(props);
|
|
63
|
+
}
|
|
64
|
+
Reactive.displayName = `canvasObserver(${Component.name || "Anonymous"})`;
|
|
65
|
+
// `memo`는 mobx `observer`가 얹어 주던 것이다. 빼면 부모가 다시 그려질 때마다
|
|
66
|
+
// 패널 전부가 따라 그려진다 — 구독이 이미 필요한 때를 알려 주므로 그건 순손해다.
|
|
67
|
+
return memo(Reactive) as ComponentType<P>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useSyncExternalStore, type RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
legacyStoreFacade,
|
|
7
|
+
type LegacyStoreFacade,
|
|
8
|
+
} from "./canvas-store-facade";
|
|
9
|
+
import { SpecResizeAbsorber } from "./spec-resize-absorber";
|
|
10
|
+
import { TableCanvasOverlay } from "./table-canvas-overlay";
|
|
11
|
+
import type { CanvasStore } from "@leviosa-ai/canvas/store";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 표·차트 오버레이를 **우리 엔진** 캔버스 위에 얹는다 (G4).
|
|
15
|
+
*
|
|
16
|
+
* 오버레이 코드는 한 줄도 안 고쳤다. 얼굴 하나(`canvas-store-facade`)와 리렌더 신호만
|
|
17
|
+
* 대 주면 그대로 돈다 — 왜 그런지는 그 파일에 적어 뒀다.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 스토어가 바뀔 때마다 새 얼굴. 안 바뀌면 같은 얼굴이라 오버레이도 안 다시 그린다. */
|
|
21
|
+
export function useLegacyStore(store: CanvasStore): LegacyStoreFacade {
|
|
22
|
+
const version = useSyncExternalStore(
|
|
23
|
+
store.subscribe,
|
|
24
|
+
() => store.version,
|
|
25
|
+
() => store.version,
|
|
26
|
+
);
|
|
27
|
+
return useMemo(() => legacyStoreFacade(store), [store, version]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function CanvasOverlayHost({
|
|
31
|
+
store,
|
|
32
|
+
containerRef,
|
|
33
|
+
}: {
|
|
34
|
+
store: CanvasStore;
|
|
35
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
36
|
+
}) {
|
|
37
|
+
const facade = useLegacyStore(store);
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<TableCanvasOverlay store={facade} containerRef={containerRef} />
|
|
41
|
+
<SpecResizeAbsorber store={facade} containerRef={containerRef} />
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SelectableElement } from "./detail-page-selection";
|
|
2
|
+
import type { StoreLike } from "../../lib/detail-page/spec-group/sync";
|
|
3
|
+
import type { CanvasStore } from "@leviosa-ai/canvas/store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 우리 엔진 스토어를 **캔버스 위 층이 보는 얼굴**로 바꾼다 (G4).
|
|
7
|
+
*
|
|
8
|
+
* 표·차트(`spec-group/sync`)와 그 오버레이들은 스토어를 구조적 타입으로만 받는다 —
|
|
9
|
+
* 스톡 SDK 클래스를 부르는 자리가 한 곳도 없다. 그래서 새 엔진을 물리는 일은 이식이
|
|
10
|
+
* 아니라 얼굴 하나 만드는 일이다. 아래 목록이 곧 **"캔버스 위 층이 스토어에서 실제로
|
|
11
|
+
* 쓰는 것 전부"**이고, G7에서 패키지 경계를 그을 때 이 목록이 그대로 계약이 된다.
|
|
12
|
+
*
|
|
13
|
+
* ## 왜 클래스를 그대로 안 넘기는가
|
|
14
|
+
*
|
|
15
|
+
* 두 가지다.
|
|
16
|
+
*
|
|
17
|
+
* 1. **리렌더.** 오버레이는 `observer`(mobx-react-lite)로 싸여 있고 그 안에 `React.memo`가
|
|
18
|
+
* 있다. 우리 스토어는 mobx가 아니라 리스너 + `version`이라 mobx가 추적할 게 없고,
|
|
19
|
+
* props identity가 그대로면 memo가 리렌더를 막는다. 버전마다 새 얼굴을 만들면
|
|
20
|
+
* memo가 통과시킨다(`useLegacyStore`).
|
|
21
|
+
* 2. **`null` vs `undefined`.** 계약은 "없으면 `undefined`"인데 우리는 `null`을 준다.
|
|
22
|
+
* 여기 한 줄로 맞춘다.
|
|
23
|
+
*
|
|
24
|
+
* ## 왜 스프레드도 `Object.create`도 아닌가
|
|
25
|
+
*
|
|
26
|
+
* `{ ...store }`는 프로토타입 메서드를 전부 잃는다(우리 메서드는 전부 프로토타입에 있다).
|
|
27
|
+
* `Object.create(store)`는 반대로 위험하다 — 메서드 안의 `this.selectedElementsIds = …`
|
|
28
|
+
* 같은 대입이 **얼굴 쪽에 그림자 속성**을 만들어 진짜 스토어와 조용히 갈라진다.
|
|
29
|
+
* 그래서 필요한 것만 손으로 적는다.
|
|
30
|
+
*/
|
|
31
|
+
export type LegacyStoreFacade = StoreLike & {
|
|
32
|
+
/** 화면 배율. 줌이 바뀌면 캔버스 위 층이 상자를 다시 잰다. */
|
|
33
|
+
scale: number;
|
|
34
|
+
selectedElementsIds: string[];
|
|
35
|
+
getElementById: (id: string) => SelectableElement | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function legacyStoreFacade(store: CanvasStore): LegacyStoreFacade {
|
|
39
|
+
return {
|
|
40
|
+
pages: store.pages,
|
|
41
|
+
activePage: store.activePage ?? undefined,
|
|
42
|
+
history: store.history,
|
|
43
|
+
scale: store.scale,
|
|
44
|
+
selectedElementsIds: store.selectedElementsIds,
|
|
45
|
+
getElementById: (id) =>
|
|
46
|
+
(store.getElementById(id) as SelectableElement | null) ?? undefined,
|
|
47
|
+
groupElements: (ids, attrs) => store.groupElements(ids, attrs),
|
|
48
|
+
deleteElements: (ids) => store.deleteElements(ids),
|
|
49
|
+
selectElements: (ids) => store.selectElements(ids),
|
|
50
|
+
};
|
|
51
|
+
}
|