@leviosa-ai/detail-page-editor 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. package/components/cardnews/color-input.tsx +9 -9
  2. package/components/detail-page/ai-generate-panel.tsx +37 -37
  3. package/components/detail-page/annotation-canvas.tsx +9 -9
  4. package/components/detail-page/annotation-dialog.tsx +18 -18
  5. package/components/detail-page/canvas-context-menu.tsx +4 -4
  6. package/components/detail-page/chart-inspector.tsx +23 -23
  7. package/components/detail-page/design-reference-picker.tsx +22 -22
  8. package/components/detail-page/detail-page-basic-shapes.tsx +4 -4
  9. package/components/detail-page/detail-page-brand-gifs-panel.tsx +10 -10
  10. package/components/detail-page/detail-page-brand-kit-panel.tsx +5 -5
  11. package/components/detail-page/detail-page-brand-panel-header.tsx +7 -4
  12. package/components/detail-page/detail-page-charts-panel.tsx +4 -4
  13. package/components/detail-page/detail-page-decorations-panel.tsx +5 -5
  14. package/components/detail-page/detail-page-download-dialog.tsx +19 -19
  15. package/components/detail-page/detail-page-editor.tsx +131 -76
  16. package/components/detail-page/detail-page-elements-panel.tsx +4 -4
  17. package/components/detail-page/detail-page-font-picker.tsx +14 -14
  18. package/components/detail-page/detail-page-history-buttons.tsx +3 -3
  19. package/components/detail-page/detail-page-host-context.tsx +63 -0
  20. package/components/detail-page/detail-page-icons-panel.tsx +13 -13
  21. package/components/detail-page/detail-page-konva-json-preview.tsx +9 -9
  22. package/components/detail-page/detail-page-layers-panel.tsx +14 -14
  23. package/components/detail-page/detail-page-my-images-panel.tsx +29 -14
  24. package/components/detail-page/detail-page-my-shapes-panel.tsx +8 -8
  25. package/components/detail-page/detail-page-page-toolbar.tsx +10 -10
  26. package/components/detail-page/detail-page-pages-panel.tsx +13 -13
  27. package/components/detail-page/detail-page-pages-timeline.tsx +4 -4
  28. package/components/detail-page/detail-page-photos-panel.tsx +13 -13
  29. package/components/detail-page/detail-page-properties-panel.tsx +55 -55
  30. package/components/detail-page/detail-page-qr-panel.tsx +13 -13
  31. package/components/detail-page/detail-page-references-panel.tsx +9 -9
  32. package/components/detail-page/detail-page-sidebar-sections.tsx +1 -1
  33. package/components/detail-page/detail-page-tables-panel.tsx +4 -4
  34. package/components/detail-page/detail-page-text-panel.tsx +4 -4
  35. package/components/detail-page/edit-quota-ui.tsx +3 -3
  36. package/components/detail-page/element-recents-strip.tsx +6 -6
  37. package/components/detail-page/fill-control.tsx +12 -12
  38. package/components/detail-page/find-replace-panel.tsx +8 -8
  39. package/components/detail-page/generation-progress-fill.tsx +4 -4
  40. package/components/detail-page/gif-effect-picker.tsx +9 -9
  41. package/components/detail-page/group-prompt-edit-panel.tsx +3 -3
  42. package/components/detail-page/inspector-controls.tsx +9 -9
  43. package/components/detail-page/panel-search-input.tsx +2 -2
  44. package/components/detail-page/prompt-edit-panel.tsx +3 -3
  45. package/components/detail-page/section-reauthor-controller.tsx +2 -2
  46. package/components/detail-page/spec-prompt-edit-panel.tsx +3 -3
  47. package/components/detail-page/svg-prompt-edit-panel.tsx +3 -3
  48. package/components/detail-page/table-inspector.tsx +24 -24
  49. package/components/ui/button.tsx +6 -6
  50. package/components/ui/command.tsx +5 -5
  51. package/components/ui/dialog.tsx +3 -3
  52. package/components/ui/label.tsx +1 -1
  53. package/components/ui/select.tsx +3 -3
  54. package/components/ui/slider.tsx +1 -1
  55. package/components/ui/tooltip.tsx +1 -1
  56. package/lib/detail-page/use-progressive-reveal.ts +82 -0
  57. package/package.json +4 -1
  58. package/styles/canvas-bridge.css +35 -0
  59. package/styles/tokens.css +103 -0
@@ -278,7 +278,7 @@ export function ColorInput({
278
278
  <button
279
279
  type="button"
280
280
  onClick={() => open ? setOpen(false) : openPopover()}
281
- className={`${swatchPx} rounded-lg border-2 border-[var(--color-border)] cursor-pointer shrink-0 transition-all hover:border-[var(--color-primary)] hover:shadow-md hover:scale-105 active:scale-95`}
281
+ className={`${swatchPx} rounded-dpe-lg border-2 border-[var(--color-border)] cursor-pointer shrink-0 transition-all hover:border-[var(--color-primary)] hover:shadow-md hover:scale-105 active:scale-95`}
282
282
  style={{
283
283
  backgroundColor: isTransparentValue ? undefined : safeColor,
284
284
  backgroundImage: isTransparentValue ? CHECKER_BG : undefined,
@@ -293,7 +293,7 @@ export function ColorInput({
293
293
  <div
294
294
  ref={popoverRef}
295
295
  data-layer-panel
296
- className={`fixed z-[200] w-56 bg-[var(--color-background)] border border-[var(--color-border)] rounded-xl shadow-lg p-3 space-y-2.5 transition-opacity duration-75 ${measured ? "opacity-100" : "opacity-0 pointer-events-none"}`}
296
+ className={`fixed z-[200] w-56 bg-[var(--color-background)] border border-[var(--color-border)] rounded-dpe-xl shadow-lg p-3 space-y-2.5 transition-opacity duration-75 ${measured ? "opacity-100" : "opacity-0 pointer-events-none"}`}
297
297
  style={{ top: popoverPos.top, left: popoverPos.left }}
298
298
  >
299
299
  {/* Transparent + Preset palette */}
@@ -302,7 +302,7 @@ export function ColorInput({
302
302
  <button
303
303
  type="button"
304
304
  onClick={() => selectColor("transparent")}
305
- className={`w-7 h-7 rounded-md border transition-transform hover:scale-110 active:scale-95 relative overflow-hidden ${
305
+ className={`w-7 h-7 rounded-dpe-md border transition-transform hover:scale-110 active:scale-95 relative overflow-hidden ${
306
306
  isTransparentValue ? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]" : "border-[var(--color-border)]"
307
307
  }`}
308
308
  style={{ backgroundImage: CHECKER_BG, backgroundSize: "8px 8px" }}
@@ -310,7 +310,7 @@ export function ColorInput({
310
310
  >
311
311
  {/* Diagonal line to indicate "none/transparent" */}
312
312
  <span className="absolute inset-0 flex items-center justify-center">
313
- <span className="block w-[130%] h-px bg-red-500 rotate-45" />
313
+ <span className="block w-[130%] h-px bg-dpe-danger-500 rotate-45" />
314
314
  </span>
315
315
  </button>
316
316
  )}
@@ -319,7 +319,7 @@ export function ColorInput({
319
319
  key={color}
320
320
  type="button"
321
321
  onClick={() => selectColor(color)}
322
- className={`w-7 h-7 rounded-md border transition-transform hover:scale-110 active:scale-95 ${
322
+ className={`w-7 h-7 rounded-dpe-md border transition-transform hover:scale-110 active:scale-95 ${
323
323
  !isTransparentValue && safeColor.toUpperCase() === color ? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]" : "border-[var(--color-border)]"
324
324
  }`}
325
325
  style={{ backgroundColor: color }}
@@ -338,13 +338,13 @@ export function ColorInput({
338
338
  key={color}
339
339
  type="button"
340
340
  onClick={() => selectColor(color)}
341
- className={`size-5 rounded-md border transition-transform hover:scale-110 active:scale-95 ${
341
+ className={`size-5 rounded-dpe-md border transition-transform hover:scale-110 active:scale-95 ${
342
342
  !isTransparentValue && safeColor.toUpperCase() === color ? "border-[var(--color-primary)] ring-1 ring-[var(--color-primary)]" : "border-[var(--color-border)]"
343
343
  }`}
344
344
  style={{ backgroundColor: color }}
345
345
  />
346
346
  ) : (
347
- <span key={idx} className="size-5 rounded-md border border-transparent" />
347
+ <span key={idx} className="size-5 rounded-dpe-md border border-transparent" />
348
348
  );
349
349
  })}
350
350
  </div>
@@ -356,7 +356,7 @@ export function ColorInput({
356
356
  <button
357
357
  type="button"
358
358
  onClick={openEyeDropper}
359
- className="flex shrink-0 items-center gap-1 rounded-lg px-1.5 py-1.5 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text-primary)]"
359
+ className="flex shrink-0 items-center gap-1 rounded-dpe-lg px-1.5 py-1.5 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text-primary)]"
360
360
  title={t("cardnews.colorInput.eyedropper")}
361
361
  >
362
362
  <Pipette className="w-3.5 h-3.5" />
@@ -369,7 +369,7 @@ export function ColorInput({
369
369
  onChange={(e) => setHexInput(e.target.value)}
370
370
  onKeyDown={(e) => e.key === "Enter" && handleHexSubmit()}
371
371
  placeholder="#000000"
372
- className="min-w-0 flex-1 rounded-lg border border-[var(--color-border)] px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-[var(--color-primary)]"
372
+ className="min-w-0 flex-1 rounded-dpe-lg border border-[var(--color-border)] px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-[var(--color-primary)]"
373
373
  />
374
374
  </div>
375
375
  </div>,
@@ -470,8 +470,8 @@ export function AiGeneratePanel({
470
470
  onClick={() => setMode("image")}
471
471
  className={
472
472
  mode === "image"
473
- ? "flex h-10 items-center justify-center gap-2 rounded-md bg-neutral-900 text-sm font-semibold text-white"
474
- : "flex h-10 items-center justify-center gap-2 rounded-md border border-neutral-200 text-sm font-medium text-neutral-600 hover:bg-neutral-50"
473
+ ? "flex h-10 items-center justify-center gap-2 rounded-dpe-md bg-dpe-ink-900 text-sm font-dpe-semibold text-dpe-on-accent"
474
+ : "flex h-10 items-center justify-center gap-2 rounded-dpe-md border border-dpe-ink-200 text-sm font-dpe-medium text-dpe-ink-600 hover:bg-dpe-ink-50"
475
475
  }
476
476
  >
477
477
  <Sparkles aria-hidden="true" size={15} />
@@ -482,8 +482,8 @@ export function AiGeneratePanel({
482
482
  onClick={() => setMode("gif")}
483
483
  className={
484
484
  isGif
485
- ? "flex h-10 items-center justify-center gap-2 rounded-md bg-neutral-900 text-sm font-semibold text-white"
486
- : "flex h-10 items-center justify-center gap-2 rounded-md border border-neutral-200 text-sm font-medium text-neutral-600 hover:bg-neutral-50"
485
+ ? "flex h-10 items-center justify-center gap-2 rounded-dpe-md bg-dpe-ink-900 text-sm font-dpe-semibold text-dpe-on-accent"
486
+ : "flex h-10 items-center justify-center gap-2 rounded-dpe-md border border-dpe-ink-200 text-sm font-dpe-medium text-dpe-ink-600 hover:bg-dpe-ink-50"
487
487
  }
488
488
  >
489
489
  <Sparkles aria-hidden="true" size={15} />
@@ -494,12 +494,12 @@ export function AiGeneratePanel({
494
494
 
495
495
  {/* GIF 모드: 배경 투명 토글(기본 ON). 상세페이지 움짤은 대체로 배경 투명. */}
496
496
  {isGif ? (
497
- <label className="flex cursor-pointer items-center justify-between rounded-md border border-neutral-200 px-3 py-2.5">
497
+ <label className="flex cursor-pointer items-center justify-between rounded-dpe-md border border-dpe-ink-200 px-3 py-2.5">
498
498
  <span className="flex flex-col">
499
- <span className="text-sm font-medium text-neutral-800">
499
+ <span className="text-sm font-dpe-medium text-dpe-ink-800">
500
500
  {t("detailPage.aiGenerate.transparentBg")}
501
501
  </span>
502
- <span className="text-[11px] leading-4 text-neutral-500">
502
+ <span className="text-[11px] leading-4 text-dpe-ink-500">
503
503
  {t("detailPage.aiGenerate.transparentBgHint")}
504
504
  </span>
505
505
  </span>
@@ -507,7 +507,7 @@ export function AiGeneratePanel({
507
507
  type="checkbox"
508
508
  checked={transparent}
509
509
  onChange={(e) => setTransparent(e.target.checked)}
510
- className="h-4 w-4 accent-neutral-900"
510
+ className="h-4 w-4 accent-dpe-ink-900"
511
511
  />
512
512
  </label>
513
513
  ) : null}
@@ -515,23 +515,23 @@ export function AiGeneratePanel({
515
515
  {/* 이미지 모델(티어) 선택 — GIF 모드는 단일 모델이라 숨긴다. */}
516
516
  <div hidden={isGif}>
517
517
  <div className="mb-2 flex items-center gap-1">
518
- <p className="text-xs font-medium text-neutral-500">{t("detailPage.aiGenerate.imageModel")}</p>
518
+ <p className="text-xs font-dpe-medium text-dpe-ink-500">{t("detailPage.aiGenerate.imageModel")}</p>
519
519
  <Tooltip>
520
520
  <TooltipTrigger asChild>
521
521
  <button
522
522
  type="button"
523
- className="flex h-4 w-4 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-600"
523
+ className="flex h-4 w-4 items-center justify-center rounded-full text-dpe-ink-400 hover:text-dpe-ink-600"
524
524
  aria-label={t("detailPage.aiGenerate.modelQualityHelp")}
525
525
  >
526
526
  <Info aria-hidden="true" size={13} />
527
527
  </button>
528
528
  </TooltipTrigger>
529
529
  <TooltipContent side="left" className="max-w-[240px] text-left">
530
- <p className="mb-1 font-semibold">{t("detailPage.aiGenerate.modelTooltipTitle")}</p>
530
+ <p className="mb-1 font-dpe-semibold">{t("detailPage.aiGenerate.modelTooltipTitle")}</p>
531
531
  <ul className="space-y-1">
532
532
  {IMAGE_TIERS.map((tk) => (
533
533
  <li key={tk}>
534
- <span className="font-medium">{IMAGE_TIER_META[tk].label}</span>
534
+ <span className="font-dpe-medium">{IMAGE_TIER_META[tk].label}</span>
535
535
  <span className="opacity-80"> · {IMAGE_TIER_META[tk].quality}</span>
536
536
  </li>
537
537
  ))}
@@ -550,20 +550,20 @@ export function AiGeneratePanel({
550
550
  return (
551
551
  <SelectItem key={tk} value={tk} className="items-start py-2">
552
552
  <div className="flex flex-col gap-0.5 pr-2">
553
- <span className="flex items-center gap-1.5 text-sm font-medium text-neutral-900">
553
+ <span className="flex items-center gap-1.5 text-sm font-dpe-medium text-dpe-ink-900">
554
554
  {meta.label}
555
555
  {meta.badge ? (
556
- <span className="rounded-full bg-neutral-900 px-1.5 py-px text-[10px] font-semibold text-white">
556
+ <span className="rounded-full bg-dpe-ink-900 px-1.5 py-px text-[10px] font-dpe-semibold text-dpe-on-accent">
557
557
  {meta.badge}
558
558
  </span>
559
559
  ) : null}
560
560
  {typeof cost === "number" && cost > 0 ? (
561
- <span className="text-[11px] font-normal text-neutral-500">
561
+ <span className="text-[11px] font-dpe-normal text-dpe-ink-500">
562
562
  · {cost}cr
563
563
  </span>
564
564
  ) : null}
565
565
  </span>
566
- <span className="text-[11px] leading-4 text-neutral-500">
566
+ <span className="text-[11px] leading-4 text-dpe-ink-500">
567
567
  {meta.quality}
568
568
  </span>
569
569
  </div>
@@ -572,7 +572,7 @@ export function AiGeneratePanel({
572
572
  })}
573
573
  </SelectContent>
574
574
  </Select>
575
- <p className="mt-1.5 text-[11px] leading-4 text-neutral-500">
575
+ <p className="mt-1.5 text-[11px] leading-4 text-dpe-ink-500">
576
576
  {IMAGE_TIER_META[tier].description}
577
577
  </p>
578
578
  </div>
@@ -584,10 +584,10 @@ export function AiGeneratePanel({
584
584
  - 좌측 자유 생성: GIF는 참조 필수(image→video), 이미지는 선택 업로드. */}
585
585
  {hasImplicitReference && implicitReferenceSrc ? (
586
586
  <div>
587
- <p className="mb-2 text-xs font-medium text-neutral-500">
587
+ <p className="mb-2 text-xs font-dpe-medium text-dpe-ink-500">
588
588
  {t("detailPage.aiGenerate.currentImageLabel")}
589
589
  </p>
590
- <div className="relative overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50">
590
+ <div className="relative overflow-hidden rounded-dpe-lg border border-dpe-ink-200 bg-dpe-ink-50">
591
591
  {/* crossOrigin: S3 이미지를 Origin 없이 그리면 ACAO 없는 응답이 캐시돼
592
592
  캔버스 crossOrigin 로드를 오염시킨다. 미리보기도 Origin을 실어 로드한다. */}
593
593
  {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -598,7 +598,7 @@ export function AiGeneratePanel({
598
598
  className="h-36 w-full object-contain"
599
599
  />
600
600
  </div>
601
- <p className="mt-1.5 text-[11px] leading-4 text-neutral-500">
601
+ <p className="mt-1.5 text-[11px] leading-4 text-dpe-ink-500">
602
602
  {isGif
603
603
  ? t("detailPage.aiGenerate.currentImageGifHint")
604
604
  : t("detailPage.aiGenerate.currentImageHint")}
@@ -610,7 +610,7 @@ export function AiGeneratePanel({
610
610
  type="button"
611
611
  onClick={() => void openAnnotate()}
612
612
  disabled={annotatePreparing || loading}
613
- className="mt-2 inline-flex h-9 w-full items-center justify-center gap-1.5 rounded-md border border-neutral-200 text-xs font-medium text-neutral-700 hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-40"
613
+ className="mt-2 inline-flex h-9 w-full items-center justify-center gap-1.5 rounded-dpe-md border border-dpe-ink-200 text-xs font-dpe-medium text-dpe-ink-700 hover:bg-dpe-ink-50 disabled:cursor-not-allowed disabled:opacity-40"
614
614
  >
615
615
  {annotatePreparing ? (
616
616
  <Loader2 aria-hidden="true" className="animate-spin" size={14} />
@@ -623,29 +623,29 @@ export function AiGeneratePanel({
623
623
  </div>
624
624
  ) : (
625
625
  <div hidden={isGif && hasImplicitReference}>
626
- <p className="mb-2 text-xs font-medium text-neutral-500">
626
+ <p className="mb-2 text-xs font-dpe-medium text-dpe-ink-500">
627
627
  {t("detailPage.aiGenerate.referenceLabel")}{" "}
628
628
  {isGif ? (
629
- <span className="text-red-500">*</span>
629
+ <span className="text-dpe-danger-500">*</span>
630
630
  ) : (
631
631
  t("detailPage.aiGenerate.referenceOptional")
632
632
  )}
633
633
  </p>
634
634
  {refUrl ? (
635
- <div className="relative overflow-hidden rounded-lg border border-neutral-200">
635
+ <div className="relative overflow-hidden rounded-dpe-lg border border-dpe-ink-200">
636
636
  {/* eslint-disable-next-line @next/next/no-img-element */}
637
637
  <img src={refUrl} crossOrigin="anonymous" alt={t("detailPage.aiGenerate.referenceAlt")} className="h-36 w-full object-cover" />
638
638
  <button
639
639
  type="button"
640
640
  onClick={() => setRefUrl(null)}
641
- className="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80"
641
+ className="absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-full bg-dpe-scrim/60 text-dpe-on-accent hover:bg-dpe-scrim/80"
642
642
  aria-label={t("detailPage.aiGenerate.removeReference")}
643
643
  >
644
644
  <X aria-hidden="true" size={15} />
645
645
  </button>
646
646
  </div>
647
647
  ) : (
648
- <label className="flex h-36 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-neutral-300 text-neutral-400 hover:border-neutral-400 hover:text-neutral-500">
648
+ <label className="flex h-36 cursor-pointer flex-col items-center justify-center gap-2 rounded-dpe-lg border border-dashed border-dpe-ink-300 text-dpe-ink-400 hover:border-dpe-ink-400 hover:text-dpe-ink-500">
649
649
  {refUploading ? (
650
650
  <Loader2 aria-hidden="true" className="animate-spin" size={22} />
651
651
  ) : (
@@ -665,35 +665,35 @@ export function AiGeneratePanel({
665
665
 
666
666
  {/* 프롬프트 */}
667
667
  <div className="flex min-h-0 flex-1 flex-col">
668
- <label className="mb-2 text-xs font-medium text-neutral-500">
669
- {t("detailPage.aiGenerate.prompt")} <span className="text-red-500">*</span>
668
+ <label className="mb-2 text-xs font-dpe-medium text-dpe-ink-500">
669
+ {t("detailPage.aiGenerate.prompt")} <span className="text-dpe-danger-500">*</span>
670
670
  </label>
671
671
  <textarea
672
672
  value={prompt}
673
673
  onChange={(e) => setPrompt(e.target.value)}
674
674
  placeholder={t("detailPage.aiGenerate.promptPlaceholder")}
675
- className="min-h-[160px] flex-1 resize-none rounded-lg border border-neutral-200 p-3 text-sm text-neutral-800 outline-none placeholder:text-neutral-400 focus:border-neutral-400"
675
+ className="min-h-[160px] flex-1 resize-none rounded-dpe-lg border border-dpe-ink-200 p-3 text-sm text-dpe-ink-800 outline-none placeholder:text-dpe-ink-400 focus:border-dpe-ink-400"
676
676
  />
677
677
  </div>
678
678
 
679
679
  {error ? (
680
- <p className="text-xs font-medium text-red-600">{error}</p>
680
+ <p className="text-xs font-dpe-medium text-dpe-danger-600">{error}</p>
681
681
  ) : null}
682
682
 
683
683
  {/* 1.5× 안전 마진 미달(또는 생성 중 크레딧 부족) → 차단 안내 + 추가구매 CTA */}
684
684
  {activeCost > 0 && (blocked || insufficient) ? (
685
- <div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2.5 text-center">
686
- <p className="text-[13px] font-medium text-amber-800">
685
+ <div className="rounded-dpe-lg border border-dpe-warn-200 bg-dpe-warn-50 px-3 py-2.5 text-center">
686
+ <p className="text-[13px] font-dpe-medium text-dpe-warn-800">
687
687
  {t("detailPage.aiGenerate.insufficientCreditsTitle")}
688
688
  </p>
689
- <p className="mt-0.5 text-[11px] leading-4 text-amber-700">
689
+ <p className="mt-0.5 text-[11px] leading-4 text-dpe-warn-700">
690
690
  {t("detailPage.aiGenerate.insufficientCreditsHint", { required, balance: creditBalance })}
691
691
  </p>
692
692
  <button
693
693
  type="button"
694
694
  onClick={onBuyCredits}
695
695
  disabled={!onBuyCredits}
696
- className="mt-2 inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-xs font-medium text-white disabled:opacity-40"
696
+ className="mt-2 inline-flex items-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-xs font-dpe-medium text-dpe-on-accent disabled:opacity-40"
697
697
  >
698
698
  <Sparkles aria-hidden="true" size={13} />
699
699
  {t("detailPage.aiGenerate.buyCredits")}
@@ -702,7 +702,7 @@ export function AiGeneratePanel({
702
702
  ) : null}
703
703
 
704
704
  {gifMissingRef ? (
705
- <p className="text-xs font-medium text-neutral-500">
705
+ <p className="text-xs font-dpe-medium text-dpe-ink-500">
706
706
  {t("detailPage.aiGenerate.gifNeedsReference")}
707
707
  </p>
708
708
  ) : null}
@@ -712,7 +712,7 @@ export function AiGeneratePanel({
712
712
  onClick={handleGenerate}
713
713
  disabled={loading || !prompt.trim() || blocked || gifMissingRef}
714
714
  className={
715
- "relative flex h-11 items-center justify-center gap-2 overflow-hidden rounded-lg bg-neutral-900 text-sm font-semibold text-white " +
715
+ "relative flex h-11 items-center justify-center gap-2 overflow-hidden rounded-dpe-lg bg-dpe-ink-900 text-sm font-dpe-semibold text-dpe-on-accent " +
716
716
  (loading
717
717
  ? "cursor-progress"
718
718
  : "disabled:cursor-not-allowed disabled:opacity-40")
@@ -738,7 +738,7 @@ export function AiGeneratePanel({
738
738
  ? t("detailPage.aiGenerate.replaceImage")
739
739
  : t("detailPage.aiGenerate.generate")}
740
740
  {activeCost > 0 && !loading ? (
741
- <span className="text-xs font-normal opacity-80">· {activeCost}cr</span>
741
+ <span className="text-xs font-dpe-normal opacity-80">· {activeCost}cr</span>
742
742
  ) : null}
743
743
  </span>
744
744
  </button>
@@ -677,7 +677,7 @@ export const AnnotationCanvas = forwardRef<
677
677
  return (
678
678
  <div
679
679
  ref={wrapRef}
680
- className="relative h-full w-full overflow-hidden bg-neutral-100"
680
+ className="relative h-full w-full overflow-hidden bg-dpe-ink-100"
681
681
  >
682
682
  {/* eslint-disable-next-line @next/next/no-img-element */}
683
683
  <img
@@ -792,7 +792,7 @@ export const AnnotationCanvas = forwardRef<
792
792
  ) : null}
793
793
 
794
794
  <div className="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center">
795
- <div className="pointer-events-auto flex items-center gap-0.5 rounded-xl border border-neutral-200 bg-white/95 px-1.5 py-1 shadow-lg backdrop-blur-sm">
795
+ <div className="pointer-events-auto flex items-center gap-0.5 rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface/95 px-1.5 py-1 shadow-lg backdrop-blur-sm">
796
796
  {TOOLS.map(([id, label, icon]) => (
797
797
  <button
798
798
  key={id}
@@ -802,16 +802,16 @@ export const AnnotationCanvas = forwardRef<
802
802
  aria-pressed={tool === id}
803
803
  onClick={() => setTool(id)}
804
804
  className={[
805
- "flex h-8 w-8 items-center justify-center rounded-lg transition-colors",
805
+ "flex h-8 w-8 items-center justify-center rounded-dpe-lg transition-colors",
806
806
  tool === id
807
- ? "bg-neutral-900 text-white"
808
- : "text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900",
807
+ ? "bg-dpe-ink-900 text-dpe-on-accent"
808
+ : "text-dpe-ink-500 hover:bg-dpe-ink-100 hover:text-dpe-ink-900",
809
809
  ].join(" ")}
810
810
  >
811
811
  {icon}
812
812
  </button>
813
813
  ))}
814
- <span className="mx-1 h-5 w-px bg-neutral-200" />
814
+ <span className="mx-1 h-5 w-px bg-dpe-ink-200" />
815
815
  {COLORS.map((c) => (
816
816
  <button
817
817
  key={c}
@@ -831,14 +831,14 @@ export const AnnotationCanvas = forwardRef<
831
831
  />
832
832
  </button>
833
833
  ))}
834
- <span className="mx-1 h-5 w-px bg-neutral-200" />
834
+ <span className="mx-1 h-5 w-px bg-dpe-ink-200" />
835
835
  <button
836
836
  type="button"
837
837
  title={labels?.undo ?? "실행 취소"}
838
838
  aria-label={labels?.undo ?? "실행 취소"}
839
839
  disabled={!history.past.length}
840
840
  onClick={undo}
841
- className="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-30"
841
+ className="flex h-8 w-8 items-center justify-center rounded-dpe-lg text-dpe-ink-500 transition-colors hover:bg-dpe-ink-100 hover:text-dpe-ink-900 disabled:cursor-not-allowed disabled:opacity-30"
842
842
  >
843
843
  <Undo2 size={16} />
844
844
  </button>
@@ -848,7 +848,7 @@ export const AnnotationCanvas = forwardRef<
848
848
  aria-label={labels?.redo ?? "다시 실행"}
849
849
  disabled={!history.future.length}
850
850
  onClick={redo}
851
- className="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-30"
851
+ className="flex h-8 w-8 items-center justify-center rounded-dpe-lg text-dpe-ink-500 transition-colors hover:bg-dpe-ink-100 hover:text-dpe-ink-900 disabled:cursor-not-allowed disabled:opacity-30"
852
852
  >
853
853
  <Redo2 size={16} />
854
854
  </button>
@@ -210,18 +210,18 @@ export function AnnotationDialog({
210
210
  const canSubmit = !busy && (hasMarks || instruction.trim().length > 0);
211
211
 
212
212
  return (
213
- <div className="fixed inset-0 z-[100] flex items-center justify-center bg-neutral-900/60 p-4">
213
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-dpe-ink-900/60 p-4">
214
214
  <div
215
215
  role="dialog"
216
216
  aria-modal="true"
217
217
  aria-label={title}
218
- className="flex h-full max-h-[92vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl"
218
+ className="flex h-full max-h-[92vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl bg-dpe-surface shadow-2xl"
219
219
  >
220
- <div className="flex items-start justify-between gap-4 border-b border-neutral-200 px-5 py-3">
220
+ <div className="flex items-start justify-between gap-4 border-b border-dpe-ink-200 px-5 py-3">
221
221
  <div className="min-w-0">
222
- <h2 className="text-sm font-semibold text-neutral-900">{title}</h2>
222
+ <h2 className="text-sm font-dpe-semibold text-dpe-ink-900">{title}</h2>
223
223
  {description ? (
224
- <p className="mt-0.5 text-xs text-neutral-500">{description}</p>
224
+ <p className="mt-0.5 text-xs text-dpe-ink-500">{description}</p>
225
225
  ) : null}
226
226
  </div>
227
227
  <button
@@ -229,7 +229,7 @@ export function AnnotationDialog({
229
229
  aria-label={t("detailPage.annotate.close", { defaultValue: "닫기" })}
230
230
  onClick={onClose}
231
231
  disabled={busy}
232
- className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-900 disabled:opacity-40"
232
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-dpe-lg text-dpe-ink-400 transition-colors hover:bg-dpe-ink-100 hover:text-dpe-ink-900 disabled:opacity-40"
233
233
  >
234
234
  <X size={16} />
235
235
  </button>
@@ -255,7 +255,7 @@ export function AnnotationDialog({
255
255
  />
256
256
  </div>
257
257
 
258
- <div className="border-t border-neutral-200 px-5 py-3">
258
+ <div className="border-t border-dpe-ink-200 px-5 py-3">
259
259
  <textarea
260
260
  value={instruction}
261
261
  onChange={(e) => setInstruction(e.target.value)}
@@ -268,16 +268,16 @@ export function AnnotationDialog({
268
268
  "표시한 곳을 어떻게 바꿀지 적어주세요. 그림 위에 손글씨로 적어도 돼요.",
269
269
  })
270
270
  }
271
- className="w-full resize-none rounded-lg border border-neutral-200 px-3 py-2 text-sm text-neutral-900 outline-none placeholder:text-neutral-400 focus:border-neutral-400 disabled:bg-neutral-50"
271
+ className="w-full resize-none rounded-dpe-lg border border-dpe-ink-200 px-3 py-2 text-sm text-dpe-ink-900 outline-none placeholder:text-dpe-ink-400 focus:border-dpe-ink-400 disabled:bg-dpe-ink-50"
272
272
  />
273
273
  {maxReferences > 0 ? (
274
274
  <div className="mt-2 flex flex-wrap items-start gap-2">
275
275
  {references.map((ref, index) => (
276
276
  <div
277
277
  key={ref.id}
278
- className="flex items-start gap-2 rounded-lg border border-neutral-200 p-1.5"
278
+ className="flex items-start gap-2 rounded-dpe-lg border border-dpe-ink-200 p-1.5"
279
279
  >
280
- <div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-md bg-neutral-100">
280
+ <div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-dpe-md bg-dpe-ink-100">
281
281
  {/* 붙인 사진은 data URI라 next/image 로 최적화할 것이 없다. */}
282
282
  {/* eslint-disable-next-line @next/next/no-img-element */}
283
283
  <img
@@ -289,7 +289,7 @@ export function AnnotationDialog({
289
289
  />
290
290
  {/* 번호는 프롬프트에 쓰는 이름이다 — 화면에 없으면 "2번 이미지"라고
291
291
  적을 근거가 없다. */}
292
- <span className="absolute left-0.5 top-0.5 rounded bg-neutral-900/75 px-1 text-[10px] font-semibold leading-4 text-white">
292
+ <span className="absolute left-0.5 top-0.5 rounded bg-dpe-ink-900/75 px-1 text-[10px] font-dpe-semibold leading-4 text-dpe-on-accent">
293
293
  {referenceOrdinal(index)}
294
294
  </span>
295
295
  <button
@@ -301,7 +301,7 @@ export function AnnotationDialog({
301
301
  aria-label={t("detailPage.reauthor.referenceRemove", {
302
302
  defaultValue: "참고 사진 빼기",
303
303
  })}
304
- className="absolute right-0.5 top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-neutral-900/70 text-white disabled:opacity-40"
304
+ className="absolute right-0.5 top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-dpe-ink-900/70 text-dpe-on-accent disabled:opacity-40"
305
305
  >
306
306
  <X size={10} />
307
307
  </button>
@@ -320,8 +320,8 @@ export function AnnotationDialog({
320
320
  onClick={() => toggleAspect(ref.id, aspect.key)}
321
321
  className={`rounded-full border px-2 py-0.5 text-[11px] transition-colors disabled:opacity-40 ${
322
322
  on
323
- ? "border-neutral-900 bg-neutral-900 text-white"
324
- : "border-neutral-200 text-neutral-500 hover:border-neutral-400 hover:text-neutral-700"
323
+ ? "border-dpe-ink-900 bg-dpe-ink-900 text-dpe-on-accent"
324
+ : "border-dpe-ink-200 text-dpe-ink-500 hover:border-dpe-ink-400 hover:text-dpe-ink-700"
325
325
  }`}
326
326
  >
327
327
  {aspect.label}
@@ -336,7 +336,7 @@ export function AnnotationDialog({
336
336
  type="button"
337
337
  disabled={busy}
338
338
  onClick={() => fileRef.current?.click()}
339
- className="flex h-[4.75rem] items-center gap-1.5 rounded-lg border border-dashed border-neutral-300 px-3 text-xs text-neutral-500 transition-colors hover:border-neutral-400 hover:text-neutral-700 disabled:opacity-40"
339
+ className="flex h-[4.75rem] items-center gap-1.5 rounded-dpe-lg border border-dashed border-dpe-ink-300 px-3 text-xs text-dpe-ink-500 transition-colors hover:border-dpe-ink-400 hover:text-dpe-ink-700 disabled:opacity-40"
340
340
  >
341
341
  <ImagePlus size={14} />
342
342
  {t("detailPage.reauthor.referenceAdd", {
@@ -353,7 +353,7 @@ export function AnnotationDialog({
353
353
  onChange={(e) => void attach(e.target.files)}
354
354
  />
355
355
  {references.length ? (
356
- <p className="w-full text-[11px] text-neutral-400">
356
+ <p className="w-full text-[11px] text-dpe-ink-400">
357
357
  {t("detailPage.reauthor.referenceHint", {
358
358
  defaultValue:
359
359
  "축을 고르면 그 장에서는 고른 것만 따라가요(안 고르면 배치와 구성만). " +
@@ -365,12 +365,12 @@ export function AnnotationDialog({
365
365
  </div>
366
366
  ) : null}
367
367
  {error || flattenError || attachError ? (
368
- <p className="mt-2 text-xs text-red-600">
368
+ <p className="mt-2 text-xs text-dpe-danger-600">
369
369
  {error ?? flattenError ?? attachError}
370
370
  </p>
371
371
  ) : null}
372
372
  <div className="mt-3 flex items-center justify-between gap-3">
373
- <p className="text-xs text-neutral-400">
373
+ <p className="text-xs text-dpe-ink-400">
374
374
  {hasMarks
375
375
  ? t("detailPage.annotate.marked", {
376
376
  defaultValue: "표시한 자리를 기준으로 고쳐요.",
@@ -200,7 +200,7 @@ export const CanvasContextMenu = observer(function CanvasContextMenu({
200
200
  role="menu"
201
201
  data-dp-canvas-menu
202
202
  style={{ left, top, width: MENU_WIDTH }}
203
- className="fixed z-[71] rounded-lg border border-neutral-200 bg-white py-1 shadow-lg"
203
+ className="fixed z-[71] rounded-dpe-lg border border-dpe-ink-200 bg-dpe-surface py-1 shadow-lg"
204
204
  >
205
205
  {items.map((item) => {
206
206
  const Icon = ICONS[item.action];
@@ -208,7 +208,7 @@ export const CanvasContextMenu = observer(function CanvasContextMenu({
208
208
  return (
209
209
  <div key={item.action}>
210
210
  {item.separated ? (
211
- <div className="my-1 border-t border-neutral-100" />
211
+ <div className="my-1 border-t border-dpe-ink-100" />
212
212
  ) : null}
213
213
  <button
214
214
  type="button"
@@ -219,9 +219,9 @@ export const CanvasContextMenu = observer(function CanvasContextMenu({
219
219
  runCanvasMenuAction(s, item.action);
220
220
  close();
221
221
  }}
222
- className="flex h-[30px] w-full items-center gap-2.5 px-3 text-left text-[13px] text-neutral-700 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
222
+ className="flex h-[30px] w-full items-center gap-2.5 px-3 text-left text-[13px] text-dpe-ink-700 transition-colors hover:bg-dpe-ink-100 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"
223
223
  >
224
- <Icon size={14} className="shrink-0 text-neutral-500" />
224
+ <Icon size={14} className="shrink-0 text-dpe-ink-500" />
225
225
  {label}
226
226
  </button>
227
227
  </div>