@leviosa-ai/detail-page-editor 0.7.3 → 0.8.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.
@@ -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>
@@ -20,6 +20,10 @@ import {
20
20
  import type { ExportDocument } from "../../lib/detail-page-canvas/export/document-model";
21
21
  import { detectGifPages } from "../../lib/detail-page-canvas/export/gif-plan";
22
22
  import { isMp4EncodeSupported } from "../../lib/detail-page-canvas/export/mp4-support";
23
+ import {
24
+ detailPageEditorProfile,
25
+ type DetailPageEditorFormat,
26
+ } from "../../lib/detail-page/editor-profile";
23
27
 
24
28
  /**
25
29
  * hookable-style 다운로드 팝오버 — 다운로드 버튼 바로 아래에 떠서 파일 형식 / 등록
@@ -62,7 +66,7 @@ const PLATFORMS = [
62
66
  { value: "general", label: "일반(범용)" },
63
67
  ] as const;
64
68
 
65
- type Format = "png" | "jpeg" | "psd" | "svg" | "ai";
69
+ type Format = DetailPageEditorFormat;
66
70
  type Scope = "all" | "current";
67
71
  type AnimationFormat = "webp" | "gif" | "mp4";
68
72
 
@@ -134,8 +138,9 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
134
138
  const { t } = useTranslation("branding");
135
139
  const host = useDetailPageHost();
136
140
  const s = store as ExportStoreLike;
141
+ const profile = detailPageEditorProfile();
137
142
  const [open, setOpen] = useState(false);
138
- const [format, setFormat] = useState<Format>("png");
143
+ const [format, setFormat] = useState<Format>(profile.exports[0]);
139
144
  const [platform, setPlatform] = useState<string>("naver");
140
145
  const [scope, setScope] = useState<Scope>("all");
141
146
  const [single, setSingle] = useState(true);
@@ -366,11 +371,15 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
366
371
  </span>
367
372
  </SelectTrigger>
368
373
  <SelectContent>
369
- <SelectItem value="png">PNG</SelectItem>
370
- <SelectItem value="jpeg">JPG</SelectItem>
371
- <SelectItem value="psd">{t("editor.formatPsd")}</SelectItem>
372
- <SelectItem value="ai">{t("editor.formatAi")}</SelectItem>
373
- <SelectItem value="svg">{t("editor.formatSvg")}</SelectItem>
374
+ {profile.exports.map((value) => (
375
+ <SelectItem key={value} value={value}>
376
+ {value === "png"
377
+ ? "PNG"
378
+ : value === "jpeg"
379
+ ? "JPG"
380
+ : t(`editor.format${value[0].toUpperCase()}${value.slice(1)}`)}
381
+ </SelectItem>
382
+ ))}
374
383
  </SelectContent>
375
384
  </Select>
376
385
  </div>
@@ -30,6 +30,7 @@ import { collectFontRequests } from "@leviosa-ai/canvas/render/use-document-font
30
30
  import { ensureCanvasKey } from "../../lib/detail-page-canvas/canvas-key";
31
31
  import { loadEditorFont } from "../../lib/detail-page-canvas/editor-fonts";
32
32
  import { normalizeDocumentAssetSrcs } from "../../lib/detail-page/asset-bytes-url";
33
+ import { selectDetailPageEditorProfile } from "../../lib/detail-page/editor-profile";
33
34
  import type { DocumentJson } from "@leviosa-ai/canvas/types";
34
35
  import { buildDetailPageSections } from "./detail-page-sidebar-sections";
35
36
  import { DetailPageProperties } from "./detail-page-properties-panel";
@@ -250,6 +251,7 @@ function buildQaSnapshot(
250
251
  document: LeviosaCanvasDocument,
251
252
  json: CanvasJson,
252
253
  ): TemplateQaSnapshot {
254
+ const slotBindings = document.slot_bindings ?? {};
253
255
  const cleanJson = stripQaOverlayElements(json);
254
256
  const pages = asArray(cleanJson.pages) as Array<Record<string, unknown>>;
255
257
  const elementById = new Map<string, { element: CanvasJsonElement; pageId?: string }>();
@@ -267,14 +269,14 @@ function buildQaSnapshot(
267
269
  }
268
270
  });
269
271
 
270
- for (const [slot, binding] of Object.entries(document.slot_bindings)) {
272
+ for (const [slot, binding] of Object.entries(slotBindings)) {
271
273
  const list = slotByElementId.get(binding.element_id) ?? [];
272
274
  list.push(slot);
273
275
  slotByElementId.set(binding.element_id, list);
274
276
  }
275
277
 
276
278
  const lint: TemplateLintIssue[] = [];
277
- const slots = Object.entries(document.slot_bindings)
279
+ const slots = Object.entries(slotBindings)
278
280
  .sort(([a], [b]) => a.localeCompare(b))
279
281
  .map(([slot, binding]) => {
280
282
  const target = elementById.get(binding.element_id);
@@ -365,7 +367,7 @@ function buildQaSnapshot(
365
367
  }
366
368
 
367
369
  for (const [slot, target] of slotsFromElements) {
368
- if (!document.slot_bindings[slot]) {
370
+ if (!slotBindings[slot]) {
369
371
  lint.push({
370
372
  level: "info",
371
373
  title: "Unregistered slot marker",
@@ -450,6 +452,7 @@ export function DetailPageEditor({
450
452
  onBack,
451
453
  headerActions,
452
454
  }: DetailPageEditorProps) {
455
+ selectDetailPageEditorProfile(initialDocument);
453
456
  const { t } = useTranslation("branding");
454
457
  // 요금제 모달은 호스트가 꽂는다 — 편집기가 열지만 무엇을 얼마에 파는지는 앱이 안다.
455
458
  const { slots } = useDetailPageHost();
@@ -18,6 +18,7 @@ import {
18
18
  requestSectionReauthor,
19
19
  subscribeSectionReauthorAvailability,
20
20
  } from "../../lib/detail-page/section-reauthor-bus";
21
+ import { detailPageEditorProfile } from "../../lib/detail-page/editor-profile";
21
22
 
22
23
  /**
23
24
  * hookable-style per-section (per-page) floating quick toolbar.
@@ -94,17 +95,25 @@ export const DetailPagePageToolbar = observer(function DetailPagePageToolbar({
94
95
  );
95
96
  const s = store as StoreLike;
96
97
  const p = page as PageLike;
98
+ const profile = detailPageEditorProfile();
97
99
  const index = s.pages.findIndex((pg) => pg.id === p.id);
98
100
  const isFirst = index <= 0;
99
101
  const isLast = index === s.pages.length - 1;
100
102
  const canDelete = s.pages.length > 1;
103
+ const canAdd = s.pages.length < profile.maxPages;
101
104
 
102
105
  const addAfter = () => {
106
+ if (!canAdd) return;
103
107
  const ref = s.activePage ?? s.pages[index];
104
108
  const next = s.addPage({
105
109
  bleed: ref?.bleed ?? 0,
106
- width: ref?.width ?? "auto",
107
- height: ref?.height ?? "auto",
110
+ width:
111
+ profile.page.width === "document"
112
+ ? ref?.width ?? "auto"
113
+ : profile.page.width,
114
+ height: profile.page.fixed
115
+ ? profile.page.height
116
+ : ref?.height ?? profile.page.height,
108
117
  });
109
118
  next.setZIndex(index + 1);
110
119
  };
@@ -153,10 +162,18 @@ export const DetailPagePageToolbar = observer(function DetailPagePageToolbar({
153
162
  <IconButton label={t("detailPage.pageToolbar.moveDown")} onClick={() => p.setZIndex(index + 1)} disabled={isLast}>
154
163
  <ChevronDown size={17} />
155
164
  </IconButton>
156
- <IconButton label={t("detailPage.pageToolbar.duplicate")} onClick={() => p.clone()}>
165
+ <IconButton
166
+ label={t("detailPage.pageToolbar.duplicate")}
167
+ onClick={() => p.clone()}
168
+ disabled={!canAdd}
169
+ >
157
170
  <Copy size={16} />
158
171
  </IconButton>
159
- <IconButton label={t("detailPage.pageToolbar.addBelow")} onClick={addAfter}>
172
+ <IconButton
173
+ label={t("detailPage.pageToolbar.addBelow")}
174
+ onClick={addAfter}
175
+ disabled={!canAdd}
176
+ >
160
177
  <Plus size={17} />
161
178
  </IconButton>
162
179
 
@@ -94,6 +94,7 @@ import {
94
94
  } from "./ai-generate-panel";
95
95
  import { toHexColor } from "../../lib/detail-page/css-color";
96
96
  import { editorAssetBase } from "../../lib/detail-page/runtime-config";
97
+ import { detailPageEditorProfile } from "../../lib/detail-page/editor-profile";
97
98
  import { setZ as setElementZ, zOrderOf } from "../../lib/detail-page/z-order";
98
99
  import {
99
100
  canDistribute,
@@ -2173,6 +2174,7 @@ const PageHeightSection = observer(function PageHeightSection({
2173
2174
  page?: PageLike;
2174
2175
  }) {
2175
2176
  const { t } = useTranslation("branding");
2177
+ const profile = detailPageEditorProfile();
2176
2178
  if (!page) return null;
2177
2179
  const height = Math.round(num(page.computedHeight, MIN_SECTION_HEIGHT));
2178
2180
  const contentBottom = sectionContentBottom(page);
@@ -2180,7 +2182,13 @@ const PageHeightSection = observer(function PageHeightSection({
2180
2182
  // "사라진" 것처럼 보인다 — 지운 게 아니라 화면 밖으로 나간 것이다.
2181
2183
  const overflow = contentBottom > height;
2182
2184
  return (
2183
- <Section title={t("detailPage.properties.pageHeight")}>
2185
+ <Section
2186
+ title={t(
2187
+ profile.wording === "section"
2188
+ ? "detailPage.properties.pageHeight"
2189
+ : "detailPage.properties.plateHeight",
2190
+ )}
2191
+ >
2184
2192
  <div className="flex items-center gap-1.5">
2185
2193
  <div className="min-w-0 flex-1">
2186
2194
  <NumberField
@@ -2222,11 +2230,12 @@ const PageInspector = observer(function PageInspector({
2222
2230
  dataGifCreditCost?: number;
2223
2231
  }) {
2224
2232
  const { t } = useTranslation("branding");
2233
+ const profile = detailPageEditorProfile();
2225
2234
  const page = store.activePage ?? store.pages[0];
2226
2235
  const bg = str(page?.background, "#ffffff");
2227
2236
  return (
2228
2237
  <>
2229
- <PageHeightSection page={page} />
2238
+ {profile.page.fixed ? null : <PageHeightSection page={page} />}
2230
2239
  <Section title={t("detailPage.properties.pageBackground")}>
2231
2240
  <FillControl value={bg} onChange={(c) => page?.set?.({ background: c })} />
2232
2241
  <p className="mt-2 text-xs text-dpe-ink-400">
@@ -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
  }
@@ -35,5 +35,13 @@
35
35
  "samlip-hopang-outline": ["heading", "outline", "rounded"],
36
36
  "paperlogy": ["heading", "body", "gothic", "angular"],
37
37
  "freesentation": ["body", "gothic"],
38
- "diphylleia": ["heading", "myeongjo"]
38
+ "diphylleia": ["heading", "myeongjo"],
39
+ "jost": ["heading", "gothic", "rounded"],
40
+ "bebas-neue": ["heading", "angular"],
41
+ "anton": ["heading", "angular"],
42
+ "archivo-black": ["heading", "gothic"],
43
+ "oswald": ["heading", "gothic", "angular"],
44
+ "playfair-display": ["heading", "myeongjo", "angular"],
45
+ "eb-garamond": ["body", "myeongjo"],
46
+ "parisienne": ["handwriting", "rounded"]
39
47
  }
package/i18n/en.json CHANGED
@@ -112,6 +112,7 @@
112
112
  "saveToMyShapes": "Save to brand shapes",
113
113
  "aiShapeEdit": "AI shape editing",
114
114
  "pageHeight": "Section height",
115
+ "plateHeight": "Plate height",
115
116
  "pageHeightFit": "Fit to content",
116
117
  "pageHeightOverflow": "Content reaches {{px}}px and overflows the bottom of this section.",
117
118
  "pageHeightHint": "You can also drag the handle below the canvas.",
package/i18n/ko.json CHANGED
@@ -112,6 +112,7 @@
112
112
  "saveToMyShapes": "브랜드 도형에 저장",
113
113
  "aiShapeEdit": "AI 도형 편집",
114
114
  "pageHeight": "섹션 높이",
115
+ "plateHeight": "판 높이",
115
116
  "pageHeightFit": "내용에 맞추기",
116
117
  "pageHeightOverflow": "내용이 {{px}}px까지 내려가 화면 아래로 넘칩니다.",
117
118
  "pageHeightHint": "캔버스 아래 손잡이를 끌어서도 바꿀 수 있어요.",
@@ -8,12 +8,12 @@
8
8
 
9
9
  import {
10
10
  FONT_CATALOG,
11
- LEVIOSA_KONVA_VERSION,
11
+ LEVIOSA_CANVAS_FONTS_VERSION,
12
12
  catalogFont,
13
13
  fontLoadSampleForText,
14
14
  resolveFontFamily,
15
15
  type CatalogFont,
16
- } from "@leviosa-ai/konva";
16
+ } from "@leviosa-ai/canvas/fonts";
17
17
 
18
18
  import { editorAssetBase } from "../detail-page/runtime-config";
19
19
 
@@ -84,7 +84,7 @@ const COMMON_HANGUL_SAMPLE =
84
84
  * `/render-fonts` 라, 아무것도 안 부른 소비자에게는 주소가 그대로다.
85
85
  */
86
86
  function bundleUrl(path: string): string {
87
- return `${editorAssetBase("fontBundle")}${path}?v=${LEVIOSA_KONVA_VERSION}`;
87
+ return `${editorAssetBase("fontBundle")}${path}?v=${LEVIOSA_CANVAS_FONTS_VERSION}`;
88
88
  }
89
89
 
90
90
  const localFontCssUrl = () => bundleUrl("/font-css.css");
@@ -0,0 +1,43 @@
1
+ import type { LeviosaCanvasDocument } from "../../types/detail-page-canvas";
2
+
3
+ export type DetailPageEditorFormat = "png" | "jpeg" | "psd" | "svg" | "ai";
4
+
5
+ export type DetailPageEditorProfile = {
6
+ page: {
7
+ width: "document" | number;
8
+ height: "auto" | number;
9
+ fixed: boolean;
10
+ };
11
+ maxPages: number;
12
+ exports: readonly DetailPageEditorFormat[];
13
+ wording: "section" | "plate";
14
+ };
15
+
16
+ const DETAIL_PAGE_PROFILE: DetailPageEditorProfile = {
17
+ page: { width: "document", height: "auto", fixed: false },
18
+ maxPages: Number.POSITIVE_INFINITY,
19
+ exports: ["png", "jpeg", "psd", "ai", "svg"],
20
+ wording: "section",
21
+ };
22
+
23
+ const CAROUSEL_PROFILE: DetailPageEditorProfile = {
24
+ page: { width: 1080, height: 1350, fixed: true },
25
+ maxPages: 10,
26
+ exports: ["jpeg"],
27
+ wording: "plate",
28
+ };
29
+
30
+ let current = DETAIL_PAGE_PROFILE;
31
+
32
+ /** 문서 종류를 읽는 유일한 자리. 종류가 없으면 기존 상세페이지 설정을 쓴다. */
33
+ export function selectDetailPageEditorProfile(
34
+ document: Pick<LeviosaCanvasDocument, "kind">,
35
+ ): DetailPageEditorProfile {
36
+ current = document.kind === "carousel" ? CAROUSEL_PROFILE : DETAIL_PAGE_PROFILE;
37
+ return current;
38
+ }
39
+
40
+ /** 편집기 입구에서 고른 설정표. 안쪽 코드는 문서 종류 대신 이 값만 읽는다. */
41
+ export function detailPageEditorProfile(): DetailPageEditorProfile {
42
+ return current;
43
+ }
@@ -41,7 +41,7 @@ export type DetailPageEditorEndpoints = {
41
41
 
42
42
  export type DetailPageEditorAssets = {
43
43
  /**
44
- * `@leviosa-ai/konva` 의 `leviosa-konva-fonts` 가 구워 두는 폰트 번들의 뿌리.
44
+ * `@leviosa-ai/canvas` 의 `leviosa-konva-fonts` 가 구워 두는 폰트 번들의 뿌리.
45
45
  * 기본 `${basePath}/render-fonts`.
46
46
  */
47
47
  fontBundle?: string;
@@ -13,7 +13,7 @@
13
13
  * 그냥 비워 보낸다 — 그 경우 서버의 폴백 폰트로 그려진다.
14
14
  */
15
15
 
16
- import { LEVIOSA_KONVA_VERSION } from "@leviosa-ai/konva";
16
+ import { LEVIOSA_CANVAS_FONTS_VERSION } from "@leviosa-ai/canvas/fonts";
17
17
 
18
18
  import {
19
19
  closestDetailPageFontWeight,
@@ -78,7 +78,7 @@ export function resolveGifWebFonts(
78
78
  const weight = closestEditorFontWeight(editorFont, face.weight);
79
79
  const url = fontBundleAbsoluteUrl(
80
80
  origin,
81
- `/family-css/${slug}-${weight}.css?v=${LEVIOSA_KONVA_VERSION}`,
81
+ `/family-css/${slug}-${weight}.css?v=${LEVIOSA_CANVAS_FONTS_VERSION}`,
82
82
  );
83
83
  // 번들이 상대 경로로 설정돼 있고 오리진도 http 면 서버가 못 받아간다 — 건너뛴다.
84
84
  if (!url) continue;
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.8.0",
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",
@@ -66,8 +66,7 @@
66
66
  "woff2-encoder": "2.0.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "@leviosa-ai/canvas": "^0.1.4",
70
- "@leviosa-ai/konva": "^0.8.0",
69
+ "@leviosa-ai/canvas": "0.3.0",
71
70
  "@tanstack/react-query": "^5.100.3",
72
71
  "konva": "^10.0.0",
73
72
  "lucide-react": "^0.563.0 || ^1.0.0",
@@ -7,13 +7,14 @@ export type CanvasSlotBinding = {
7
7
  export type LeviosaCanvasDocument = {
8
8
  schema_version: "leviosa-canvas-detail-page-v1";
9
9
  renderer: "leviosa_canvas_detail_page";
10
+ kind?: "detail-page" | "carousel";
10
11
  template_id?: string | null;
11
12
  template_version?: number | null;
12
13
  canvas: {
13
14
  width: number;
14
15
  background: string;
15
16
  };
16
- slot_bindings: Record<string, CanvasSlotBinding>;
17
+ slot_bindings?: Record<string, CanvasSlotBinding>;
17
18
  canvas_json: Record<string, unknown>;
18
19
  fonts: Array<Record<string, unknown>>;
19
20
  source: "leviosa_canvas_editor";