@leviosa-ai/detail-page-editor 0.2.2 → 0.3.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.
- package/components/cardnews/color-input.tsx +9 -9
- package/components/detail-page/ai-generate-panel.tsx +37 -37
- package/components/detail-page/annotation-canvas.tsx +9 -9
- package/components/detail-page/annotation-dialog.tsx +18 -18
- package/components/detail-page/canvas-context-menu.tsx +4 -4
- package/components/detail-page/chart-inspector.tsx +23 -23
- package/components/detail-page/design-reference-picker.tsx +22 -22
- package/components/detail-page/detail-page-basic-shapes.tsx +4 -4
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +10 -10
- package/components/detail-page/detail-page-brand-kit-panel.tsx +5 -5
- package/components/detail-page/detail-page-brand-panel-header.tsx +7 -4
- package/components/detail-page/detail-page-charts-panel.tsx +4 -4
- package/components/detail-page/detail-page-decorations-panel.tsx +5 -5
- package/components/detail-page/detail-page-download-dialog.tsx +19 -19
- package/components/detail-page/detail-page-editor.tsx +131 -76
- package/components/detail-page/detail-page-elements-panel.tsx +4 -4
- package/components/detail-page/detail-page-font-picker.tsx +14 -14
- package/components/detail-page/detail-page-history-buttons.tsx +3 -3
- package/components/detail-page/detail-page-host-context.tsx +63 -0
- package/components/detail-page/detail-page-icons-panel.tsx +13 -13
- package/components/detail-page/detail-page-konva-json-preview.tsx +9 -9
- package/components/detail-page/detail-page-layers-panel.tsx +14 -14
- package/components/detail-page/detail-page-my-images-panel.tsx +29 -14
- package/components/detail-page/detail-page-my-shapes-panel.tsx +8 -8
- package/components/detail-page/detail-page-page-toolbar.tsx +10 -10
- package/components/detail-page/detail-page-pages-panel.tsx +13 -13
- package/components/detail-page/detail-page-pages-timeline.tsx +4 -4
- package/components/detail-page/detail-page-photos-panel.tsx +13 -13
- package/components/detail-page/detail-page-properties-panel.tsx +55 -55
- package/components/detail-page/detail-page-qr-panel.tsx +13 -13
- package/components/detail-page/detail-page-references-panel.tsx +9 -9
- package/components/detail-page/detail-page-sidebar-sections.tsx +1 -1
- package/components/detail-page/detail-page-tables-panel.tsx +4 -4
- package/components/detail-page/detail-page-text-panel.tsx +4 -4
- package/components/detail-page/edit-quota-ui.tsx +3 -3
- package/components/detail-page/element-recents-strip.tsx +6 -6
- package/components/detail-page/fill-control.tsx +12 -12
- package/components/detail-page/find-replace-panel.tsx +8 -8
- package/components/detail-page/generation-progress-fill.tsx +4 -4
- package/components/detail-page/gif-effect-picker.tsx +9 -9
- package/components/detail-page/group-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/inspector-controls.tsx +9 -9
- package/components/detail-page/panel-search-input.tsx +2 -2
- package/components/detail-page/prompt-edit-panel.tsx +3 -3
- package/components/detail-page/section-reauthor-controller.tsx +2 -2
- package/components/detail-page/spec-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/svg-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/table-inspector.tsx +24 -24
- package/components/ui/button.tsx +6 -6
- package/components/ui/command.tsx +5 -5
- package/components/ui/dialog.tsx +3 -3
- package/components/ui/label.tsx +1 -1
- package/components/ui/select.tsx +3 -3
- package/components/ui/slider.tsx +1 -1
- package/components/ui/tooltip.tsx +1 -1
- package/lib/detail-page/use-progressive-reveal.ts +82 -0
- package/lib/detail-page-canvas/konva-fallback.ts +10 -4
- package/package.json +4 -1
- package/styles/canvas-bridge.css +35 -0
- package/styles/tokens.css +103 -0
|
@@ -329,7 +329,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
329
329
|
onClick={() => setOpen((v) => !v)}
|
|
330
330
|
aria-haspopup="dialog"
|
|
331
331
|
aria-expanded={open}
|
|
332
|
-
className="inline-flex h-9 items-center gap-2 rounded-md bg-
|
|
332
|
+
className="inline-flex h-9 items-center gap-2 rounded-dpe-md bg-dpe-ink-900 px-3 text-sm font-dpe-semibold text-dpe-on-accent transition-colors hover:bg-dpe-ink-800"
|
|
333
333
|
>
|
|
334
334
|
<Download aria-hidden="true" size={16} />
|
|
335
335
|
{t("editor.download")}
|
|
@@ -339,17 +339,17 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
339
339
|
<div
|
|
340
340
|
role="dialog"
|
|
341
341
|
aria-label={t("editor.downloadTitle")}
|
|
342
|
-
className="absolute right-0 top-[calc(100%+8px)] z-50 w-80 rounded-xl border border-
|
|
342
|
+
className="absolute right-0 top-[calc(100%+8px)] z-50 w-80 rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface p-4 shadow-[0_12px_40px_rgba(0,0,0,0.16)]"
|
|
343
343
|
>
|
|
344
344
|
<div className="mb-3 flex items-center justify-between">
|
|
345
|
-
<h3 className="text-base font-semibold text-
|
|
345
|
+
<h3 className="text-base font-dpe-semibold text-dpe-ink-950">
|
|
346
346
|
{t("editor.downloadTitle")}
|
|
347
347
|
</h3>
|
|
348
348
|
<button
|
|
349
349
|
type="button"
|
|
350
350
|
onClick={() => setOpen(false)}
|
|
351
351
|
aria-label={t("editor.close")}
|
|
352
|
-
className="flex h-7 w-7 items-center justify-center rounded-md text-
|
|
352
|
+
className="flex h-7 w-7 items-center justify-center rounded-dpe-md text-dpe-ink-400 hover:bg-dpe-ink-100 hover:text-dpe-ink-700"
|
|
353
353
|
>
|
|
354
354
|
<X size={16} />
|
|
355
355
|
</button>
|
|
@@ -357,11 +357,11 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
357
357
|
|
|
358
358
|
<div className="space-y-3">
|
|
359
359
|
<div className="space-y-1.5">
|
|
360
|
-
<Label className="text-xs text-
|
|
360
|
+
<Label className="text-xs text-dpe-ink-500">{t("editor.fileFormat")}</Label>
|
|
361
361
|
<Select value={format} onValueChange={(v) => setFormat(v as Format)}>
|
|
362
362
|
<SelectTrigger className="w-full">
|
|
363
363
|
<span className="inline-flex items-center gap-2">
|
|
364
|
-
<ImageIcon size={15} className="text-
|
|
364
|
+
<ImageIcon size={15} className="text-dpe-ink-500" />
|
|
365
365
|
<SelectValue />
|
|
366
366
|
</span>
|
|
367
367
|
</SelectTrigger>
|
|
@@ -376,7 +376,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
376
376
|
</div>
|
|
377
377
|
|
|
378
378
|
<div className="space-y-1.5">
|
|
379
|
-
<Label className="text-xs text-
|
|
379
|
+
<Label className="text-xs text-dpe-ink-500">
|
|
380
380
|
{t("editor.registerPlatform")}
|
|
381
381
|
</Label>
|
|
382
382
|
<Select value={platform} onValueChange={setPlatform}>
|
|
@@ -394,7 +394,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
394
394
|
</div>
|
|
395
395
|
|
|
396
396
|
<div className="space-y-1.5">
|
|
397
|
-
<Label className="text-xs text-
|
|
397
|
+
<Label className="text-xs text-dpe-ink-500">{t("editor.pageScope")}</Label>
|
|
398
398
|
<Select value={scope} onValueChange={(v) => setScope(v as Scope)}>
|
|
399
399
|
<SelectTrigger className="w-full">
|
|
400
400
|
<SelectValue />
|
|
@@ -412,7 +412,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
412
412
|
{format !== "psd" ? (
|
|
413
413
|
<label className="flex cursor-pointer items-center gap-2.5 py-0.5">
|
|
414
414
|
<Switch checked={single} onCheckedChange={setSingle} />
|
|
415
|
-
<span className="text-sm text-
|
|
415
|
+
<span className="text-sm text-dpe-ink-700">
|
|
416
416
|
{format === "ai"
|
|
417
417
|
? t("editor.mergeArtboard")
|
|
418
418
|
: format === "svg"
|
|
@@ -425,8 +425,8 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
425
425
|
{isRasterFormat(format) ? (
|
|
426
426
|
<div className="space-y-1.5">
|
|
427
427
|
<div className="flex items-center justify-between">
|
|
428
|
-
<Label className="text-xs text-
|
|
429
|
-
<span className="text-xs font-semibold text-
|
|
428
|
+
<Label className="text-xs text-dpe-ink-500">{t("editor.resolution")}</Label>
|
|
429
|
+
<span className="text-xs font-dpe-semibold text-dpe-ink-700">{resolution}×</span>
|
|
430
430
|
</div>
|
|
431
431
|
<Slider
|
|
432
432
|
min={1}
|
|
@@ -435,7 +435,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
435
435
|
value={[resolution]}
|
|
436
436
|
onValueChange={(v) => setResolution(v[0] ?? 1)}
|
|
437
437
|
/>
|
|
438
|
-
<div className="flex justify-between text-[10px] text-
|
|
438
|
+
<div className="flex justify-between text-[10px] text-dpe-ink-400">
|
|
439
439
|
<span>1×</span>
|
|
440
440
|
<span>4×</span>
|
|
441
441
|
</div>
|
|
@@ -447,7 +447,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
447
447
|
선택은 픽셀 형식일 때만 의미가 있다. */}
|
|
448
448
|
{hasAnimation && isRasterFormat(format) ? (
|
|
449
449
|
<div className="space-y-1.5">
|
|
450
|
-
<Label className="text-xs text-
|
|
450
|
+
<Label className="text-xs text-dpe-ink-500">
|
|
451
451
|
{t("editor.animationFormat")}
|
|
452
452
|
</Label>
|
|
453
453
|
<Select
|
|
@@ -465,7 +465,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
465
465
|
))}
|
|
466
466
|
</SelectContent>
|
|
467
467
|
</Select>
|
|
468
|
-
<p className="text-[10px] leading-relaxed text-
|
|
468
|
+
<p className="text-[10px] leading-relaxed text-dpe-ink-400">
|
|
469
469
|
{t(
|
|
470
470
|
ANIMATION_FORMATS.find((f) => f.value === animationFormat)?.hintKey ??
|
|
471
471
|
"",
|
|
@@ -474,8 +474,8 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
474
474
|
</div>
|
|
475
475
|
) : null}
|
|
476
476
|
|
|
477
|
-
<div className="rounded-lg bg-
|
|
478
|
-
<p className="mb-1 font-semibold text-
|
|
477
|
+
<div className="rounded-dpe-lg bg-dpe-ink-50 p-3 text-xs text-dpe-ink-600">
|
|
478
|
+
<p className="mb-1 font-dpe-semibold text-dpe-ink-700">{t("editor.downloadInfo")}</p>
|
|
479
479
|
<ul className="space-y-0.5">
|
|
480
480
|
<li>• {t("editor.pagesCount", { count: selectedPages.length })}</li>
|
|
481
481
|
<li>
|
|
@@ -488,7 +488,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
488
488
|
{format === "svg" ? <li>• {t("editor.svgNote")}</li> : null}
|
|
489
489
|
{format === "ai" ? <li>• {t("editor.aiNote")}</li> : null}
|
|
490
490
|
{aiOverflow ? (
|
|
491
|
-
<li className="text-
|
|
491
|
+
<li className="text-dpe-warn-700">
|
|
492
492
|
• {t("editor.aiOverflowNote", { limit: AI_MAX_ARTBOARD.toLocaleString() })}
|
|
493
493
|
</li>
|
|
494
494
|
) : null}
|
|
@@ -496,14 +496,14 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
|
|
|
496
496
|
</div>
|
|
497
497
|
|
|
498
498
|
{error ? (
|
|
499
|
-
<p className="text-xs font-medium text-
|
|
499
|
+
<p className="text-xs font-dpe-medium text-dpe-danger-600">{error}</p>
|
|
500
500
|
) : null}
|
|
501
501
|
|
|
502
502
|
<Button
|
|
503
503
|
type="button"
|
|
504
504
|
onClick={handleExport}
|
|
505
505
|
disabled={exporting || selectedPages.length === 0}
|
|
506
|
-
className="h-11 w-full bg-
|
|
506
|
+
className="h-11 w-full bg-dpe-ink-900 text-sm font-dpe-semibold hover:bg-dpe-ink-800"
|
|
507
507
|
>
|
|
508
508
|
{exporting ? (
|
|
509
509
|
<>
|
|
@@ -444,6 +444,10 @@ export function DetailPageEditor({
|
|
|
444
444
|
const { t } = useTranslation("branding");
|
|
445
445
|
// 요금제 모달은 호스트가 꽂는다 — 편집기가 열지만 무엇을 얼마에 파는지는 앱이 안다.
|
|
446
446
|
const { slots } = useDetailPageHost();
|
|
447
|
+
// 영역 슬롯. 색·모서리는 토큰으로 바꾸지만, 무엇이 어디에 놓이는가는 앱이 정한다.
|
|
448
|
+
const SidebarSlot = slots?.EditorSidebar;
|
|
449
|
+
const HeaderSlot = slots?.EditorHeader;
|
|
450
|
+
const InspectorSlot = slots?.EditorInspector;
|
|
447
451
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
448
452
|
const [saveOk, setSaveOk] = useState(false);
|
|
449
453
|
// 편집 한도 소진 시 "편집 크레딧 추가하기" → pricing 모달을 인플레이스로 연다.
|
|
@@ -682,61 +686,72 @@ export function DetailPageEditor({
|
|
|
682
686
|
return (
|
|
683
687
|
<div className="absolute inset-0 flex">
|
|
684
688
|
{hotkeys}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
689
|
+
{SidebarSlot ? (
|
|
690
|
+
<SidebarSlot
|
|
691
|
+
store={store}
|
|
692
|
+
sections={sidebarSections as never}
|
|
693
|
+
defaultSection="pages"
|
|
694
|
+
/>
|
|
695
|
+
) : (
|
|
696
|
+
<LeviosaSidePanel
|
|
697
|
+
store={store}
|
|
698
|
+
sections={sidebarSections as never}
|
|
699
|
+
defaultSection="pages"
|
|
700
|
+
/>
|
|
701
|
+
)}
|
|
690
702
|
<div className="relative min-w-0 flex-1">
|
|
691
703
|
<LeviosaCanvasWorkspace store={store} gap={4}>
|
|
692
704
|
{findReplace}
|
|
693
705
|
<DetailPagePagesTimeline store={store} />
|
|
694
706
|
</LeviosaCanvasWorkspace>
|
|
695
|
-
<div className="absolute bottom-16 left-1/2 z-20 -translate-x-1/2 rounded-lg border border-
|
|
707
|
+
<div className="absolute bottom-16 left-1/2 z-20 -translate-x-1/2 rounded-dpe-lg border border-dpe-ink-200 bg-dpe-surface/95 px-2 py-1 shadow-sm backdrop-blur-sm">
|
|
696
708
|
<LeviosaZoomButtons store={store} />
|
|
697
709
|
</div>
|
|
698
710
|
</div>
|
|
699
711
|
</div>
|
|
700
712
|
);
|
|
701
|
-
}, [store, sidebarSections]);
|
|
713
|
+
}, [store, sidebarSections, SidebarSlot]);
|
|
702
714
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<header
|
|
715
|
+
const historyPart = <DetailPageHistoryButtons store={store} />;
|
|
716
|
+
const downloadPart = (
|
|
717
|
+
<DetailPageDownloadDialog
|
|
718
|
+
store={store}
|
|
719
|
+
fileName={initialDocument.template_id ?? "detail-page"}
|
|
720
|
+
slotBindings={initialDocument.slot_bindings}
|
|
721
|
+
/>
|
|
722
|
+
);
|
|
723
|
+
const defaultHeader = (
|
|
724
|
+
<header
|
|
725
|
+
data-dpe-part="header"
|
|
726
|
+
className="sticky top-0 z-20 flex h-14 shrink-0 items-center gap-1.5 border-b border-dpe-ink-200 bg-dpe-surface px-3"
|
|
727
|
+
>
|
|
713
728
|
{onBack ? (
|
|
714
729
|
<button
|
|
715
730
|
type="button"
|
|
716
731
|
onClick={onBack}
|
|
717
732
|
aria-label={t("editor.back")}
|
|
718
733
|
title={t("editor.back")}
|
|
719
|
-
className="flex h-9 w-9 items-center justify-center rounded-md text-
|
|
734
|
+
className="flex h-9 w-9 items-center justify-center rounded-dpe-md text-dpe-ink-600 hover:bg-dpe-ink-100 hover:text-dpe-ink-900"
|
|
720
735
|
>
|
|
721
736
|
<ChevronLeft aria-hidden="true" size={20} />
|
|
722
737
|
</button>
|
|
723
738
|
) : null}
|
|
724
|
-
<p className="ml-1 max-w-[280px] truncate text-sm font-semibold text-
|
|
739
|
+
<p className="ml-1 max-w-[280px] truncate text-sm font-dpe-semibold text-dpe-ink-900">
|
|
725
740
|
{productName?.trim() || t("editor.untitled")}
|
|
726
741
|
</p>
|
|
727
742
|
|
|
728
743
|
<div className="mx-auto" />
|
|
729
744
|
|
|
730
|
-
|
|
731
|
-
<span className="mx-1 h-5 w-px bg-
|
|
745
|
+
{historyPart}
|
|
746
|
+
<span className="mx-1 h-5 w-px bg-dpe-ink-200" aria-hidden="true" />
|
|
732
747
|
|
|
733
748
|
{saveOk ? (
|
|
734
|
-
<span className="hidden text-xs font-medium text-
|
|
749
|
+
<span className="hidden text-xs font-dpe-medium text-dpe-ok-600 sm:inline">
|
|
735
750
|
{t("editor.saved")}
|
|
736
751
|
</span>
|
|
737
752
|
) : null}
|
|
738
753
|
{saveError ? (
|
|
739
|
-
<span className="hidden max-w-[180px] truncate text-xs font-medium text-
|
|
754
|
+
<span className="hidden max-w-[180px] truncate text-xs font-dpe-medium text-dpe-danger-600 sm:inline">
|
|
740
755
|
{saveError}
|
|
741
756
|
</span>
|
|
742
757
|
) : null}
|
|
@@ -744,30 +759,26 @@ export function DetailPageEditor({
|
|
|
744
759
|
type="button"
|
|
745
760
|
onClick={handleSave}
|
|
746
761
|
disabled={saving}
|
|
747
|
-
className="inline-flex h-9 items-center gap-2 rounded-md border border-
|
|
762
|
+
className="inline-flex h-9 items-center gap-2 rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface px-3 text-sm font-dpe-semibold text-dpe-ink-900 hover:bg-dpe-ink-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
748
763
|
>
|
|
749
764
|
<Save aria-hidden="true" size={16} />
|
|
750
765
|
{saving ? t("editor.saving") : t("editor.save")}
|
|
751
766
|
</button>
|
|
752
|
-
|
|
753
|
-
store={store}
|
|
754
|
-
fileName={initialDocument.template_id ?? "detail-page"}
|
|
755
|
-
slotBindings={initialDocument.slot_bindings}
|
|
756
|
-
/>
|
|
767
|
+
{downloadPart}
|
|
757
768
|
|
|
758
769
|
{headerActions ? (
|
|
759
770
|
<>
|
|
760
|
-
<span className="mx-1 h-5 w-px bg-
|
|
771
|
+
<span className="mx-1 h-5 w-px bg-dpe-ink-200" aria-hidden="true" />
|
|
761
772
|
{headerActions}
|
|
762
773
|
</>
|
|
763
774
|
) : null}
|
|
764
775
|
</header>
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
776
|
+
);
|
|
777
|
+
const defaultInspector = (
|
|
778
|
+
<aside
|
|
779
|
+
data-dpe-part="inspector"
|
|
780
|
+
className="flex min-h-0 flex-col border-l border-dpe-ink-200 bg-dpe-surface"
|
|
781
|
+
>
|
|
771
782
|
{/* Figma-style properties inspector — 상단 툴바 대신 오른쪽에 둔다. */}
|
|
772
783
|
<div className="min-h-0 flex-1">
|
|
773
784
|
<DetailPageProperties
|
|
@@ -791,19 +802,19 @@ export function DetailPageEditor({
|
|
|
791
802
|
</div>
|
|
792
803
|
|
|
793
804
|
{/* Template QA / slot inspector / lint — authoring tools, not seller-facing. */}
|
|
794
|
-
<details className="border-t border-
|
|
795
|
-
<summary className="cursor-pointer p-4 text-xs font-semibold uppercase tracking-[0.06em] text-
|
|
805
|
+
<details className="border-t border-dpe-ink-200">
|
|
806
|
+
<summary className="cursor-pointer p-4 text-xs font-dpe-semibold uppercase tracking-[0.06em] text-dpe-ink-400 hover:text-dpe-ink-600">
|
|
796
807
|
개발자 도구 · 템플릿 QA
|
|
797
808
|
</summary>
|
|
798
809
|
<div className="flex max-h-[55vh] min-h-0 flex-col overflow-y-auto">
|
|
799
|
-
<div className="border-b border-
|
|
810
|
+
<div className="border-b border-dpe-ink-200 p-4">
|
|
800
811
|
<div className="flex items-center justify-between gap-3">
|
|
801
812
|
<div>
|
|
802
|
-
<h2 className="flex items-center gap-2 text-sm font-semibold text-
|
|
813
|
+
<h2 className="flex items-center gap-2 text-sm font-dpe-semibold text-dpe-ink-950">
|
|
803
814
|
<ListChecks aria-hidden="true" size={16} />
|
|
804
815
|
{t("editor.qaTitle")}
|
|
805
816
|
</h2>
|
|
806
|
-
<p className="mt-1 text-xs text-
|
|
817
|
+
<p className="mt-1 text-xs text-dpe-ink-500">
|
|
807
818
|
{qaSnapshot.counts.pages} pages / {qaSnapshot.counts.elements} elements /{" "}
|
|
808
819
|
{qaSnapshot.counts.slots} slots
|
|
809
820
|
</p>
|
|
@@ -811,31 +822,31 @@ export function DetailPageEditor({
|
|
|
811
822
|
<button
|
|
812
823
|
type="button"
|
|
813
824
|
onClick={handleRefreshQa}
|
|
814
|
-
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-
|
|
825
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-dpe-md border border-dpe-ink-200 text-dpe-ink-600 hover:bg-dpe-ink-50"
|
|
815
826
|
title={t("editor.refreshQa")}
|
|
816
827
|
>
|
|
817
828
|
<RefreshCw aria-hidden="true" size={15} />
|
|
818
829
|
</button>
|
|
819
830
|
</div>
|
|
820
831
|
<div className="mt-4 grid grid-cols-3 gap-2 text-center text-xs">
|
|
821
|
-
<div className="rounded-md border border-
|
|
822
|
-
<div className="text-base font-semibold text-
|
|
823
|
-
<div className="text-
|
|
832
|
+
<div className="rounded-dpe-md border border-dpe-ink-200 p-2">
|
|
833
|
+
<div className="text-base font-dpe-semibold text-dpe-ink-950">{qaSnapshot.counts.slots}</div>
|
|
834
|
+
<div className="text-dpe-ink-500">slots</div>
|
|
824
835
|
</div>
|
|
825
|
-
<div className="rounded-md border border-
|
|
826
|
-
<div className="text-base font-semibold text-
|
|
827
|
-
<div className="text-
|
|
836
|
+
<div className="rounded-dpe-md border border-dpe-danger-100 bg-dpe-danger-50 p-2">
|
|
837
|
+
<div className="text-base font-dpe-semibold text-dpe-danger-700">{qaSnapshot.counts.errors}</div>
|
|
838
|
+
<div className="text-dpe-danger-600">errors</div>
|
|
828
839
|
</div>
|
|
829
|
-
<div className="rounded-md border border-
|
|
830
|
-
<div className="text-base font-semibold text-
|
|
831
|
-
<div className="text-
|
|
840
|
+
<div className="rounded-dpe-md border border-dpe-warn-100 bg-dpe-warn-50 p-2">
|
|
841
|
+
<div className="text-base font-dpe-semibold text-dpe-warn-700">{qaSnapshot.counts.warnings}</div>
|
|
842
|
+
<div className="text-dpe-warn-600">warnings</div>
|
|
832
843
|
</div>
|
|
833
844
|
</div>
|
|
834
845
|
<div className="mt-4 flex gap-2">
|
|
835
846
|
<button
|
|
836
847
|
type="button"
|
|
837
848
|
onClick={() => setOverlayEnabled((value) => !value)}
|
|
838
|
-
className="inline-flex h-9 flex-1 items-center justify-center gap-2 rounded-md border border-
|
|
849
|
+
className="inline-flex h-9 flex-1 items-center justify-center gap-2 rounded-dpe-md border border-dpe-ink-200 text-xs font-dpe-semibold text-dpe-ink-700 hover:bg-dpe-ink-50"
|
|
839
850
|
>
|
|
840
851
|
{overlayEnabled ? <EyeOff aria-hidden="true" size={15} /> : <Eye aria-hidden="true" size={15} />}
|
|
841
852
|
{overlayEnabled ? t("editor.overlayOff") : t("editor.overlayOn")}
|
|
@@ -843,7 +854,7 @@ export function DetailPageEditor({
|
|
|
843
854
|
<button
|
|
844
855
|
type="button"
|
|
845
856
|
onClick={handleCopySummary}
|
|
846
|
-
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-
|
|
857
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-dpe-md border border-dpe-ink-200 text-dpe-ink-700 hover:bg-dpe-ink-50"
|
|
847
858
|
title={t("editor.copySummary")}
|
|
848
859
|
>
|
|
849
860
|
<Copy aria-hidden="true" size={15} />
|
|
@@ -851,20 +862,20 @@ export function DetailPageEditor({
|
|
|
851
862
|
<button
|
|
852
863
|
type="button"
|
|
853
864
|
onClick={handleDownloadSummary}
|
|
854
|
-
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-
|
|
865
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-dpe-md border border-dpe-ink-200 text-dpe-ink-700 hover:bg-dpe-ink-50"
|
|
855
866
|
title={t("editor.downloadSummary")}
|
|
856
867
|
>
|
|
857
868
|
<Download aria-hidden="true" size={15} />
|
|
858
869
|
</button>
|
|
859
870
|
</div>
|
|
860
871
|
{summaryCopied ? (
|
|
861
|
-
<p className="mt-2 text-xs font-medium text-
|
|
872
|
+
<p className="mt-2 text-xs font-dpe-medium text-dpe-ok-600">{t("editor.summaryCopied")}</p>
|
|
862
873
|
) : null}
|
|
863
874
|
</div>
|
|
864
875
|
|
|
865
876
|
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
|
866
877
|
<section>
|
|
867
|
-
<h3 className="text-xs font-semibold uppercase tracking-[0.08em] text-
|
|
878
|
+
<h3 className="text-xs font-dpe-semibold uppercase tracking-[0.08em] text-dpe-ink-500">
|
|
868
879
|
{t("editor.slotInspector")}
|
|
869
880
|
</h3>
|
|
870
881
|
<div className="mt-3 space-y-2">
|
|
@@ -874,31 +885,31 @@ export function DetailPageEditor({
|
|
|
874
885
|
key={slot.slot}
|
|
875
886
|
type="button"
|
|
876
887
|
onClick={() => handleSelectSlot(slot)}
|
|
877
|
-
className="w-full rounded-md border border-
|
|
888
|
+
className="w-full rounded-dpe-md border border-dpe-ink-200 p-3 text-left hover:border-dpe-ink-300 hover:bg-dpe-ink-50"
|
|
878
889
|
>
|
|
879
890
|
<div className="flex items-center justify-between gap-2">
|
|
880
|
-
<span className="truncate text-sm font-semibold text-
|
|
891
|
+
<span className="truncate text-sm font-dpe-semibold text-dpe-ink-950">{slot.slot}</span>
|
|
881
892
|
<span
|
|
882
893
|
className={
|
|
883
894
|
slot.state === "error"
|
|
884
|
-
? "text-xs font-semibold text-
|
|
895
|
+
? "text-xs font-dpe-semibold text-dpe-danger-600"
|
|
885
896
|
: slot.state === "warning"
|
|
886
|
-
? "text-xs font-semibold text-
|
|
887
|
-
: "text-xs font-semibold text-
|
|
897
|
+
? "text-xs font-dpe-semibold text-dpe-warn-600"
|
|
898
|
+
: "text-xs font-dpe-semibold text-dpe-ok-600"
|
|
888
899
|
}
|
|
889
900
|
>
|
|
890
901
|
{slot.state}
|
|
891
902
|
</span>
|
|
892
903
|
</div>
|
|
893
|
-
<p className="mt-1 truncate text-xs text-
|
|
904
|
+
<p className="mt-1 truncate text-xs text-dpe-ink-500">
|
|
894
905
|
{slot.binding?.kind ?? "-"} / {slot.binding?.element_id ?? "-"}
|
|
895
906
|
</p>
|
|
896
|
-
<p className="mt-1 text-xs text-
|
|
897
|
-
<p className="mt-2 text-xs text-
|
|
907
|
+
<p className="mt-1 text-xs text-dpe-ink-600">{formatRect(slot.element)}</p>
|
|
908
|
+
<p className="mt-2 text-xs text-dpe-ink-500">{slot.message}</p>
|
|
898
909
|
</button>
|
|
899
910
|
))
|
|
900
911
|
) : (
|
|
901
|
-
<p className="rounded-md border border-
|
|
912
|
+
<p className="rounded-dpe-md border border-dpe-ink-200 p-3 text-sm text-dpe-ink-500">
|
|
902
913
|
{t("editor.noSlots")}
|
|
903
914
|
</p>
|
|
904
915
|
)}
|
|
@@ -906,7 +917,7 @@ export function DetailPageEditor({
|
|
|
906
917
|
</section>
|
|
907
918
|
|
|
908
919
|
<section className="mt-6">
|
|
909
|
-
<h3 className="text-xs font-semibold uppercase tracking-[0.08em] text-
|
|
920
|
+
<h3 className="text-xs font-dpe-semibold uppercase tracking-[0.08em] text-dpe-ink-500">
|
|
910
921
|
{t("editor.templateLint")}
|
|
911
922
|
</h3>
|
|
912
923
|
<div className="mt-3 space-y-2">
|
|
@@ -914,32 +925,32 @@ export function DetailPageEditor({
|
|
|
914
925
|
qaSnapshot.lint.map((issue, index) => (
|
|
915
926
|
<div
|
|
916
927
|
key={`${issue.title}-${issue.slot ?? issue.elementId ?? index}`}
|
|
917
|
-
className="rounded-md border border-
|
|
928
|
+
className="rounded-dpe-md border border-dpe-ink-200 p-3"
|
|
918
929
|
>
|
|
919
930
|
<div className="flex items-center justify-between gap-2">
|
|
920
|
-
<p className="text-sm font-semibold text-
|
|
931
|
+
<p className="text-sm font-dpe-semibold text-dpe-ink-950">{issue.title}</p>
|
|
921
932
|
<span
|
|
922
933
|
className={
|
|
923
934
|
issue.level === "error"
|
|
924
|
-
? "text-xs font-semibold text-
|
|
935
|
+
? "text-xs font-dpe-semibold text-dpe-danger-600"
|
|
925
936
|
: issue.level === "warning"
|
|
926
|
-
? "text-xs font-semibold text-
|
|
927
|
-
: "text-xs font-semibold text-
|
|
937
|
+
? "text-xs font-dpe-semibold text-dpe-warn-600"
|
|
938
|
+
: "text-xs font-dpe-semibold text-dpe-active-600"
|
|
928
939
|
}
|
|
929
940
|
>
|
|
930
941
|
{issue.level}
|
|
931
942
|
</span>
|
|
932
943
|
</div>
|
|
933
|
-
<p className="mt-1 text-xs leading-5 text-
|
|
944
|
+
<p className="mt-1 text-xs leading-5 text-dpe-ink-600">{issue.detail}</p>
|
|
934
945
|
{issue.slot || issue.elementId ? (
|
|
935
|
-
<p className="mt-2 truncate text-xs text-
|
|
946
|
+
<p className="mt-2 truncate text-xs text-dpe-ink-400">
|
|
936
947
|
{[issue.slot, issue.elementId].filter(Boolean).join(" / ")}
|
|
937
948
|
</p>
|
|
938
949
|
) : null}
|
|
939
950
|
</div>
|
|
940
951
|
))
|
|
941
952
|
) : (
|
|
942
|
-
<div className="rounded-md border border-
|
|
953
|
+
<div className="rounded-dpe-md border border-dpe-ok-100 bg-dpe-ok-50 p-3 text-sm font-dpe-medium text-dpe-ok-700">
|
|
943
954
|
{t("editor.lintPassed")}
|
|
944
955
|
</div>
|
|
945
956
|
)}
|
|
@@ -947,10 +958,10 @@ export function DetailPageEditor({
|
|
|
947
958
|
</section>
|
|
948
959
|
|
|
949
960
|
<section className="mt-6">
|
|
950
|
-
<h3 className="text-xs font-semibold uppercase tracking-[0.08em] text-
|
|
961
|
+
<h3 className="text-xs font-dpe-semibold uppercase tracking-[0.08em] text-dpe-ink-500">
|
|
951
962
|
{t("editor.designerSummary")}
|
|
952
963
|
</h3>
|
|
953
|
-
<pre className="mt-3 max-h-72 overflow-auto rounded-md border border-
|
|
964
|
+
<pre className="mt-3 max-h-72 overflow-auto rounded-dpe-md border border-dpe-ink-200 bg-dpe-ink-50 p-3 text-xs leading-5 text-dpe-ink-700">
|
|
954
965
|
{qaSnapshot.summary}
|
|
955
966
|
</pre>
|
|
956
967
|
</section>
|
|
@@ -958,6 +969,50 @@ export function DetailPageEditor({
|
|
|
958
969
|
</div>
|
|
959
970
|
</details>
|
|
960
971
|
</aside>
|
|
972
|
+
);
|
|
973
|
+
// 인스펙터는 감싸 쓰는 경우가 더 많다 — 자기 크롬만 두르고 속은 그대로 둔다.
|
|
974
|
+
const inspector = InspectorSlot ? (
|
|
975
|
+
<InspectorSlot store={store} defaultInspector={defaultInspector} />
|
|
976
|
+
) : (
|
|
977
|
+
defaultInspector
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const header = HeaderSlot ? (
|
|
981
|
+
<HeaderSlot
|
|
982
|
+
productName={productName?.trim() || t("editor.untitled")}
|
|
983
|
+
onBack={onBack}
|
|
984
|
+
save={{ run: handleSave, saving, ok: saveOk, error: saveError }}
|
|
985
|
+
parts={{
|
|
986
|
+
history: historyPart,
|
|
987
|
+
download: downloadPart,
|
|
988
|
+
actions: headerActions ?? null,
|
|
989
|
+
}}
|
|
990
|
+
/>
|
|
991
|
+
) : (
|
|
992
|
+
defaultHeader
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
return (
|
|
996
|
+
// 꽂혀 있으면 `observer`로 싼 패널·오버레이가 이 스토어의 변경 신호를 받는다
|
|
997
|
+
// (canvas-observer.tsx).
|
|
998
|
+
<CanvasStoreContext.Provider value={store}>
|
|
999
|
+
<div
|
|
1000
|
+
data-dpe-root=""
|
|
1001
|
+
className="flex h-screen min-h-[640px] flex-col bg-dpe-ink-100"
|
|
1002
|
+
>
|
|
1003
|
+
{/* hookable식 상단 헤더: 뒤로가기 · 상품명 · (되돌리기/다시실행) · 저장 ·
|
|
1004
|
+
다운로드 · 앱 공용 크롬(크레딧/알림/언어, 호스트 주입). 높이를 고정하고
|
|
1005
|
+
본문은 flex-1 min-h-0으로 두어, 헤더 높이가 바뀌어도 캔버스가 남는 높이를
|
|
1006
|
+
정확히 채워 페이지 하단이 잘리지 않는다. */}
|
|
1007
|
+
{header}
|
|
1008
|
+
<div className="grid min-h-0 flex-1 grid-cols-[1fr_360px]">
|
|
1009
|
+
{/* 캔버스 칸의 높이를 못 박는다. 안 그러면 그리드 행이 내용만큼 늘어나(높이
|
|
1010
|
+
100%가 auto로 풀린다) 작업 영역이 화면 아래로 자라고, 아래 붙는 배율·화면
|
|
1011
|
+
띠가 화면 밖으로 밀린다. */}
|
|
1012
|
+
<div data-dpe-part="workspace" className="relative min-w-0 overflow-hidden">
|
|
1013
|
+
{canvas}
|
|
1014
|
+
</div>
|
|
1015
|
+
{inspector}
|
|
961
1016
|
</div>
|
|
962
1017
|
{/* 화면 하나만 마크업째 다시 저작 — 요청은 캔버스 옆 페이지 툴바에서 온다. */}
|
|
963
1018
|
<SectionReauthorController
|
|
@@ -80,7 +80,7 @@ export function DetailPageElementsPanel({
|
|
|
80
80
|
<div
|
|
81
81
|
role="tablist"
|
|
82
82
|
aria-label={t("detailPage.sidebar.elements")}
|
|
83
|
-
className="grid shrink-0 grid-cols-3 gap-0.5 border-b border-
|
|
83
|
+
className="grid shrink-0 grid-cols-3 gap-0.5 border-b border-dpe-ink-200 p-2"
|
|
84
84
|
>
|
|
85
85
|
{GROUPS.map(({ id, labelKey, Icon }) => {
|
|
86
86
|
const active = id === group;
|
|
@@ -91,10 +91,10 @@ export function DetailPageElementsPanel({
|
|
|
91
91
|
role="tab"
|
|
92
92
|
aria-selected={active}
|
|
93
93
|
onClick={() => select(id)}
|
|
94
|
-
className={`flex flex-col items-center justify-center gap-0.5 rounded-md px-1 py-1.5 text-[11px] font-medium transition-colors ${
|
|
94
|
+
className={`flex flex-col items-center justify-center gap-0.5 rounded-dpe-md px-1 py-1.5 text-[11px] font-dpe-medium transition-colors ${
|
|
95
95
|
active
|
|
96
|
-
? "bg-
|
|
97
|
-
: "text-
|
|
96
|
+
? "bg-dpe-active-50 text-dpe-active-700"
|
|
97
|
+
: "text-dpe-ink-500 hover:bg-dpe-ink-100"
|
|
98
98
|
}`}
|
|
99
99
|
>
|
|
100
100
|
<Icon aria-hidden="true" size={14} />
|