@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.
- package/components/detail-page/canvas-selection-tools.tsx +19 -4
- package/components/detail-page/detail-page-download-dialog.tsx +16 -7
- package/components/detail-page/detail-page-editor.tsx +6 -3
- package/components/detail-page/detail-page-page-toolbar.tsx +21 -4
- package/components/detail-page/detail-page-properties-panel.tsx +11 -2
- package/components/detail-page/selection-quick-toolbar.tsx +44 -1
- package/config/detail-page-bundle-font-tags.json +9 -1
- package/i18n/en.json +1 -0
- package/i18n/ko.json +1 -0
- package/lib/cardnews/font-loader.ts +3 -3
- package/lib/detail-page/editor-profile.ts +43 -0
- package/lib/detail-page/runtime-config.ts +1 -1
- package/lib/detail-page-canvas/gif-web-fonts.ts +2 -2
- package/package.json +2 -3
- package/types/detail-page-canvas.ts +2 -1
|
@@ -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 {
|
|
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
|
-
|
|
54
|
-
|
|
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 =
|
|
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>(
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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(
|
|
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(
|
|
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 (!
|
|
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:
|
|
107
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
11
|
+
LEVIOSA_CANVAS_FONTS_VERSION,
|
|
12
12
|
catalogFont,
|
|
13
13
|
fontLoadSampleForText,
|
|
14
14
|
resolveFontFamily,
|
|
15
15
|
type CatalogFont,
|
|
16
|
-
} from "@leviosa-ai/
|
|
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=${
|
|
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/
|
|
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 {
|
|
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=${
|
|
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.
|
|
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": "
|
|
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
|
|
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";
|