@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, useState, useSyncExternalStore } from "react";
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 [editingBackground, setEditingBackground] = useState(false);
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
- <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>
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="mt-2 border-t border-dpe-ink-100 pt-2"
258
- onClick={(event) => event.stopPropagation()}
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
- <FillControl
261
- value={background}
262
- onChange={(next) => page.set?.({ background: next })}
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
- ) : null}
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.0",
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",