@leviosa-ai/detail-page-editor 0.11.0 → 0.11.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.
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { detailPageEditorProfile } from "../../lib/detail-page/editor-profile";
|
|
4
4
|
|
|
5
|
-
import { Fragment, useCallback,
|
|
5
|
+
import { Fragment, useCallback, useSyncExternalStore } from "react";
|
|
6
6
|
import { observer } from "./canvas-observer";
|
|
7
7
|
import { useTranslation } from "react-i18next";
|
|
8
|
-
import { GripVertical, Plus } from "lucide-react";
|
|
8
|
+
import { Copy, GripVertical, Plus, Trash2 } from "lucide-react";
|
|
9
9
|
import {
|
|
10
10
|
DndContext,
|
|
11
11
|
closestCenter,
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
import { CSS } from "@dnd-kit/utilities";
|
|
24
24
|
|
|
25
25
|
import { detailPageThumbnailBus } from "./detail-page-thumbnail-bus";
|
|
26
|
-
import { FillControl } from "./fill-control";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Custom Canvas side-panel "페이지" section (hookable-style page list).
|
|
@@ -53,6 +52,7 @@ type PageLike = {
|
|
|
53
52
|
/** 단색 hex 또는 `linear-gradient(...)` 문자열. `FillControl`이 둘 다 만든다. */
|
|
54
53
|
background?: string;
|
|
55
54
|
set?: (props: Record<string, unknown>) => void;
|
|
55
|
+
clone?: () => void;
|
|
56
56
|
/** 문서 안 몇 번째인가를 바꾼다. 페이지 자신이 가진 함수다 — 스토어에는 없다. */
|
|
57
57
|
setZIndex?: (index: number) => void;
|
|
58
58
|
};
|
|
@@ -61,6 +61,7 @@ type StoreLike = {
|
|
|
61
61
|
activePage?: PageLike;
|
|
62
62
|
selectPage: (id: string) => void;
|
|
63
63
|
addPage: (props: Record<string, unknown>) => PageLike;
|
|
64
|
+
deletePages?: (ids: string[]) => void;
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
type TFn = (key: string, opts?: Record<string, unknown>) => string;
|
|
@@ -115,11 +116,11 @@ function InsertHere({
|
|
|
115
116
|
const next = store.addPage({
|
|
116
117
|
width:
|
|
117
118
|
profile.page.width === "document"
|
|
118
|
-
? ref?.computedWidth ?? "auto"
|
|
119
|
+
? (ref?.computedWidth ?? "auto")
|
|
119
120
|
: profile.page.width,
|
|
120
121
|
height: profile.page.fixed
|
|
121
122
|
? profile.page.height
|
|
122
|
-
: ref?.computedHeight ?? profile.page.height,
|
|
123
|
+
: (ref?.computedHeight ?? profile.page.height),
|
|
123
124
|
});
|
|
124
125
|
next.setZIndex?.(after + 1);
|
|
125
126
|
store.selectPage(next.id);
|
|
@@ -170,8 +171,7 @@ const PageRow = observer(function PageRow({
|
|
|
170
171
|
const active = store.activePage?.id === page.id;
|
|
171
172
|
const ratio = page.computedHeight / Math.max(1, page.computedWidth);
|
|
172
173
|
const { role, title } = pageLabel(page, index, t);
|
|
173
|
-
const
|
|
174
|
-
const background = page.background || "#ffffff";
|
|
174
|
+
const maxPages = detailPageEditorProfile().maxPages;
|
|
175
175
|
|
|
176
176
|
const style: React.CSSProperties = {
|
|
177
177
|
transform: CSS.Transform.toString(transform),
|
|
@@ -199,70 +199,71 @@ const PageRow = observer(function PageRow({
|
|
|
199
199
|
].join(" ")}
|
|
200
200
|
>
|
|
201
201
|
<div className="flex items-center gap-2">
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<div
|
|
213
|
-
className="relative shrink-0 overflow-hidden rounded-dpe-md border border-dpe-ink-100 bg-dpe-ink-50"
|
|
214
|
-
style={{ width: 64, height: Math.min(80, Math.max(40, 64 * ratio)) }}
|
|
215
|
-
>
|
|
216
|
-
{thumb ? (
|
|
217
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
218
|
-
<img
|
|
219
|
-
src={thumb}
|
|
220
|
-
alt={title || t("detailPage.pages.pageN", { number: index + 1 })}
|
|
221
|
-
draggable={false}
|
|
222
|
-
className="h-full w-full object-cover object-top"
|
|
223
|
-
/>
|
|
224
|
-
) : null}
|
|
225
|
-
</div>
|
|
226
|
-
<div className="min-w-0 flex-1">
|
|
227
|
-
<p className="truncate text-sm">
|
|
228
|
-
<span className="text-dpe-ink-400">{role}</span>
|
|
229
|
-
{title ? (
|
|
230
|
-
<>
|
|
231
|
-
<span className="mx-1 text-dpe-ink-300">|</span>
|
|
232
|
-
<span className="font-dpe-bold text-dpe-ink-900">{title}</span>
|
|
233
|
-
</>
|
|
234
|
-
) : null}
|
|
235
|
-
</p>
|
|
236
|
-
</div>
|
|
237
|
-
{/*
|
|
238
|
-
페이지 배경. 우측 인스펙터에도 같은 컨트롤이 있지만 **아무것도 선택 안 됐을 때만**
|
|
239
|
-
뜬다 — 20섹션짜리 문서를 만지는 동안 선택이 비는 순간이 거의 없어서 사실상 안 보였다.
|
|
240
|
-
쓰기는 우측과 똑같이 `page.set({background})`이라 상태가 갈라지지 않는다.
|
|
241
|
-
*/}
|
|
242
|
-
<button
|
|
243
|
-
type="button"
|
|
244
|
-
onClick={(event) => {
|
|
245
|
-
event.stopPropagation();
|
|
246
|
-
setEditingBackground((open) => !open);
|
|
247
|
-
}}
|
|
248
|
-
aria-label={t("detailPage.pages.background")}
|
|
249
|
-
aria-expanded={editingBackground}
|
|
250
|
-
title={t("detailPage.pages.background")}
|
|
251
|
-
className="h-6 w-6 shrink-0 rounded-dpe-md border border-dpe-ink-200 hover:border-dpe-ink-400"
|
|
252
|
-
style={{ background }}
|
|
253
|
-
/>
|
|
254
|
-
</div>
|
|
255
|
-
{editingBackground ? (
|
|
202
|
+
<span
|
|
203
|
+
{...listeners}
|
|
204
|
+
aria-label={t("detailPage.pages.reorderHandle")}
|
|
205
|
+
className={[
|
|
206
|
+
"shrink-0 touch-none rounded text-dpe-ink-300 hover:text-dpe-ink-500",
|
|
207
|
+
isDragging ? "cursor-grabbing" : "cursor-grab",
|
|
208
|
+
].join(" ")}
|
|
209
|
+
>
|
|
210
|
+
<GripVertical aria-hidden="true" size={16} />
|
|
211
|
+
</span>
|
|
256
212
|
<div
|
|
257
|
-
className="
|
|
258
|
-
|
|
213
|
+
className="relative shrink-0 overflow-hidden rounded-dpe-md border border-dpe-ink-100 bg-dpe-ink-50"
|
|
214
|
+
style={{ width: 64, height: Math.min(80, Math.max(40, 64 * ratio)) }}
|
|
259
215
|
>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
216
|
+
{thumb ? (
|
|
217
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
218
|
+
<img
|
|
219
|
+
src={thumb}
|
|
220
|
+
alt={title || t("detailPage.pages.pageN", { number: index + 1 })}
|
|
221
|
+
draggable={false}
|
|
222
|
+
className="h-full w-full object-cover object-top"
|
|
223
|
+
/>
|
|
224
|
+
) : null}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="min-w-0 flex-1">
|
|
227
|
+
<p className="truncate text-sm">
|
|
228
|
+
<span className="text-dpe-ink-400">{role}</span>
|
|
229
|
+
{title ? (
|
|
230
|
+
<>
|
|
231
|
+
<span className="mx-1 text-dpe-ink-300">|</span>
|
|
232
|
+
<span className="font-dpe-bold text-dpe-ink-900">{title}</span>
|
|
233
|
+
</>
|
|
234
|
+
) : null}
|
|
235
|
+
</p>
|
|
264
236
|
</div>
|
|
265
|
-
|
|
237
|
+
{/* 복제 / 삭제. 판을 고르지 않아도 목록에서 바로 되게 둔다. */}
|
|
238
|
+
<div className="flex shrink-0 flex-col gap-1">
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
disabled={store.pages.length >= maxPages}
|
|
242
|
+
onClick={(event) => {
|
|
243
|
+
event.stopPropagation();
|
|
244
|
+
page.clone?.();
|
|
245
|
+
}}
|
|
246
|
+
aria-label={t("detailPage.pageToolbar.duplicate")}
|
|
247
|
+
title={t("detailPage.pageToolbar.duplicate")}
|
|
248
|
+
className="flex h-6 w-6 items-center justify-center rounded-dpe-md border border-dpe-ink-200 text-dpe-ink-500 hover:border-dpe-ink-400 hover:text-dpe-ink-800 disabled:cursor-not-allowed disabled:text-dpe-ink-200 disabled:hover:border-dpe-ink-200"
|
|
249
|
+
>
|
|
250
|
+
<Copy aria-hidden="true" size={13} />
|
|
251
|
+
</button>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
disabled={store.pages.length <= 1}
|
|
255
|
+
onClick={(event) => {
|
|
256
|
+
event.stopPropagation();
|
|
257
|
+
store.deletePages?.([page.id]);
|
|
258
|
+
}}
|
|
259
|
+
aria-label={t("detailPage.pageToolbar.delete")}
|
|
260
|
+
title={t("detailPage.pageToolbar.delete")}
|
|
261
|
+
className="flex h-6 w-6 items-center justify-center rounded-dpe-md border border-dpe-ink-200 text-dpe-ink-500 hover:border-dpe-ink-400 hover:text-dpe-danger-600 disabled:cursor-not-allowed disabled:text-dpe-ink-200 disabled:hover:border-dpe-ink-200"
|
|
262
|
+
>
|
|
263
|
+
<Trash2 aria-hidden="true" size={13} />
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
266
267
|
</div>
|
|
267
268
|
);
|
|
268
269
|
});
|
|
@@ -315,7 +316,7 @@ export const DetailPagePagesPanel = observer(function DetailPagePagesPanel({
|
|
|
315
316
|
? Math.round(
|
|
316
317
|
typeof profile.page.height === "number"
|
|
317
318
|
? profile.page.height
|
|
318
|
-
: s.pages[0]?.computedHeight ?? 0,
|
|
319
|
+
: (s.pages[0]?.computedHeight ?? 0),
|
|
319
320
|
)
|
|
320
321
|
: Math.round(s.pages.reduce((acc, p) => acc + p.computedHeight, 0));
|
|
321
322
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leviosa-ai/detail-page-editor",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.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",
|