@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
@@ -426,10 +426,10 @@ function AlignButton({
426
426
  aria-pressed={active}
427
427
  disabled={disabled}
428
428
  onClick={onClick}
429
- className={`flex h-8 flex-1 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-30 ${
429
+ className={`flex h-8 flex-1 items-center justify-center rounded-dpe-md border transition-colors disabled:cursor-not-allowed disabled:opacity-30 ${
430
430
  active
431
- ? "border-neutral-300 bg-neutral-100 text-neutral-900"
432
- : "border-neutral-200 bg-white text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900"
431
+ ? "border-dpe-ink-300 bg-dpe-ink-100 text-dpe-ink-900"
432
+ : "border-dpe-ink-200 bg-dpe-surface text-dpe-ink-600 hover:bg-dpe-ink-50 hover:text-dpe-ink-900"
433
433
  }`}
434
434
  >
435
435
  {children}
@@ -555,7 +555,7 @@ const OrderSection = observer(function OrderSection({ els }: { els: ElementLike[
555
555
  aria-label={title}
556
556
  disabled={disabled}
557
557
  onClick={onClick}
558
- className="flex h-8 flex-1 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-600 transition-colors hover:bg-neutral-50 hover:text-neutral-900 disabled:cursor-not-allowed disabled:opacity-30"
558
+ className="flex h-8 flex-1 items-center justify-center rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface text-dpe-ink-600 transition-colors hover:bg-dpe-ink-50 hover:text-dpe-ink-900 disabled:cursor-not-allowed disabled:opacity-30"
559
559
  >
560
560
  {children}
561
561
  </button>
@@ -576,7 +576,7 @@ const OrderSection = observer(function OrderSection({ els }: { els: ElementLike[
576
576
  <Btn title={t("detailPage.properties.sendToBack")} disabled={atBack} onClick={() => setZ(0)}>
577
577
  <ChevronsDown size={15} />
578
578
  </Btn>
579
- <span className="ml-1 shrink-0 rounded-md bg-neutral-100 px-1.5 py-0.5 text-[11px] font-medium tabular-nums text-neutral-500">
579
+ <span className="ml-1 shrink-0 rounded-dpe-md bg-dpe-ink-100 px-1.5 py-0.5 text-[11px] font-dpe-medium tabular-nums text-dpe-ink-500">
580
580
  {z + 1}/{count}
581
581
  </span>
582
582
  </div>
@@ -601,9 +601,9 @@ const OpacityRow = observer(function OpacityRow({ els }: { els: ElementLike[] })
601
601
  max={100}
602
602
  value={Math.round(opacity * 100)}
603
603
  onChange={(e) => setAll(els, { opacity: Number(e.target.value) / 100 })}
604
- className="min-w-0 flex-1 accent-neutral-900"
604
+ className="min-w-0 flex-1 accent-dpe-ink-900"
605
605
  />
606
- <span className="w-10 text-right text-sm tabular-nums text-neutral-700">
606
+ <span className="w-10 text-right text-sm tabular-nums text-dpe-ink-700">
607
607
  {Math.round(opacity * 100)}%
608
608
  </span>
609
609
  </div>
@@ -651,7 +651,7 @@ function DeleteRow({ store, els }: { store: StoreLike; els: ElementLike[] }) {
651
651
  <button
652
652
  type="button"
653
653
  onClick={() => store.ungroupElements?.([els[0].id])}
654
- className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md border border-neutral-200 bg-white text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
654
+ className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface text-sm font-dpe-semibold text-dpe-ink-700 hover:bg-dpe-ink-50"
655
655
  >
656
656
  <Ungroup aria-hidden="true" size={15} />
657
657
  {t("detailPage.properties.ungroup")}
@@ -660,7 +660,7 @@ function DeleteRow({ store, els }: { store: StoreLike; els: ElementLike[] }) {
660
660
  <button
661
661
  type="button"
662
662
  onClick={() => store.deleteElements?.(els.map((e) => e.id))}
663
- className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md border border-red-200 bg-red-50 text-sm font-semibold text-red-600 hover:bg-red-100"
663
+ className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-dpe-md border border-dpe-danger-200 bg-dpe-danger-50 text-sm font-dpe-semibold text-dpe-danger-600 hover:bg-dpe-danger-100"
664
664
  >
665
665
  <Trash2 aria-hidden="true" size={15} />
666
666
  {t("detailPage.properties.delete")}
@@ -789,7 +789,7 @@ const TextInspector = observer(function TextInspector({
789
789
  value={str(single.text)}
790
790
  onChange={(e) => single.set({ text: e.target.value })}
791
791
  rows={3}
792
- className="w-full resize-y rounded-md border border-neutral-200 bg-white px-2 py-2 text-sm text-neutral-900 outline-none focus:border-neutral-400"
792
+ className="w-full resize-y rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface px-2 py-2 text-sm text-dpe-ink-900 outline-none focus:border-dpe-ink-400"
793
793
  />
794
794
  {/* 실서비스(생성 ID 존재)면 모든 텍스트에 프롬프트 편집을 노출한다. 예전에는
795
795
  custom.leviosaSlot(카피 슬롯)이 있어야만 떴는데, 헤드라인처럼 슬롯 배정을
@@ -837,7 +837,7 @@ const TextInspector = observer(function TextInspector({
837
837
  value={displayedWeight}
838
838
  disabled={fontBusy}
839
839
  onChange={(event) => void applyFontWeight(Number(event.target.value))}
840
- className="h-8 min-w-0 flex-1 rounded-md border border-neutral-200 bg-white px-2 text-xs text-neutral-700 outline-none focus:border-neutral-400 disabled:opacity-50"
840
+ className="h-8 min-w-0 flex-1 rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface px-2 text-xs text-dpe-ink-700 outline-none focus:border-dpe-ink-400 disabled:opacity-50"
841
841
  >
842
842
  {fontWeights.map((weight) => (
843
843
  <option key={weight} value={weight}>
@@ -916,7 +916,7 @@ const TextInspector = observer(function TextInspector({
916
916
  onChange={(c) => setHighlight(els, c)}
917
917
  />
918
918
  ) : (
919
- <span className="text-[11px] text-neutral-400">
919
+ <span className="text-[11px] text-dpe-ink-400">
920
920
  {t("detailPage.properties.highlightHint")}
921
921
  </span>
922
922
  )}
@@ -926,7 +926,7 @@ const TextInspector = observer(function TextInspector({
926
926
  <Section title={t("detailPage.properties.spacing")}>
927
927
  <div className="grid grid-cols-2 gap-2">
928
928
  <label className="flex items-center gap-2">
929
- <span className="w-10 text-xs text-neutral-500">{t("detailPage.properties.lineHeight")}</span>
929
+ <span className="w-10 text-xs text-dpe-ink-500">{t("detailPage.properties.lineHeight")}</span>
930
930
  <NumberField
931
931
  value={lineHeight}
932
932
  step={0.1}
@@ -935,7 +935,7 @@ const TextInspector = observer(function TextInspector({
935
935
  />
936
936
  </label>
937
937
  <label className="flex items-center gap-2">
938
- <span className="w-10 text-xs text-neutral-500">{t("detailPage.properties.letterSpacing")}</span>
938
+ <span className="w-10 text-xs text-dpe-ink-500">{t("detailPage.properties.letterSpacing")}</span>
939
939
  <NumberField
940
940
  value={letterSpacing}
941
941
  step={0.5}
@@ -1153,7 +1153,7 @@ const BgRemoveSection = observer(function BgRemoveSection({
1153
1153
  type="button"
1154
1154
  onClick={run}
1155
1155
  disabled={busy}
1156
- className="inline-flex w-full items-center justify-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
1156
+ className="inline-flex w-full items-center justify-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-sm font-dpe-medium text-dpe-on-accent transition hover:bg-dpe-ink-700 disabled:cursor-not-allowed disabled:opacity-50"
1157
1157
  >
1158
1158
  <Eraser size={14} />
1159
1159
  {busy
@@ -1166,9 +1166,9 @@ const BgRemoveSection = observer(function BgRemoveSection({
1166
1166
  {!busy && creditCost ? ` · ${creditCost}` : ""}
1167
1167
  </button>
1168
1168
  {error ? (
1169
- <p className="mt-1.5 text-[11px] text-red-500">{error}</p>
1169
+ <p className="mt-1.5 text-[11px] text-dpe-danger-500">{error}</p>
1170
1170
  ) : (
1171
- <p className="mt-1.5 text-[11px] text-neutral-400">
1171
+ <p className="mt-1.5 text-[11px] text-dpe-ink-400">
1172
1172
  {t("detailPage.properties.bgRemoveHint", {
1173
1173
  defaultValue:
1174
1174
  "피사체만 남기고 배경을 투명하게 만들어요. 자리와 크기는 그대로예요.",
@@ -1309,7 +1309,7 @@ const ImageGifSection = observer(function ImageGifSection({
1309
1309
  type="button"
1310
1310
  onClick={run}
1311
1311
  disabled={busy}
1312
- className="inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
1312
+ className="inline-flex items-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-sm font-dpe-medium text-dpe-on-accent transition hover:bg-dpe-ink-700 disabled:cursor-not-allowed disabled:opacity-50"
1313
1313
  >
1314
1314
  <Film size={14} />
1315
1315
  {busy
@@ -1324,18 +1324,18 @@ const ImageGifSection = observer(function ImageGifSection({
1324
1324
  <button
1325
1325
  type="button"
1326
1326
  onClick={() => abortRef.current?.abort()}
1327
- className="mt-1.5 text-[11px] text-neutral-400 underline hover:text-neutral-600"
1327
+ className="mt-1.5 text-[11px] text-dpe-ink-400 underline hover:text-dpe-ink-600"
1328
1328
  >
1329
1329
  {t("detailPage.properties.imageGifCancel", {
1330
1330
  defaultValue: "취소 (만들던 GIF는 '내 이미지'에 저장돼요)",
1331
1331
  })}
1332
1332
  </button>
1333
1333
  ) : error ? (
1334
- <p className="mt-1.5 text-[11px] text-red-500">{error}</p>
1334
+ <p className="mt-1.5 text-[11px] text-dpe-danger-500">{error}</p>
1335
1335
  ) : notice ? (
1336
- <p className="mt-1.5 text-[11px] text-amber-600">{notice}</p>
1336
+ <p className="mt-1.5 text-[11px] text-dpe-warn-600">{notice}</p>
1337
1337
  ) : (
1338
- <p className="mt-1.5 text-[11px] text-neutral-400">
1338
+ <p className="mt-1.5 text-[11px] text-dpe-ink-400">
1339
1339
  {selected?.hint ?? hint ?? ""}
1340
1340
  </p>
1341
1341
  )}
@@ -1506,7 +1506,7 @@ const TextGifSection = observer(function TextGifSection({
1506
1506
  type="button"
1507
1507
  onClick={run}
1508
1508
  disabled={busy || !first}
1509
- className="inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
1509
+ className="inline-flex items-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-sm font-dpe-medium text-dpe-on-accent transition hover:bg-dpe-ink-700 disabled:cursor-not-allowed disabled:opacity-50"
1510
1510
  >
1511
1511
  <Film size={14} />
1512
1512
  {busy
@@ -1516,9 +1516,9 @@ const TextGifSection = observer(function TextGifSection({
1516
1516
  </button>
1517
1517
  </div>
1518
1518
  {error ? (
1519
- <p className="mt-1.5 text-[11px] text-red-500">{error}</p>
1519
+ <p className="mt-1.5 text-[11px] text-dpe-danger-500">{error}</p>
1520
1520
  ) : (
1521
- <p className="mt-1.5 text-[11px] text-neutral-400">
1521
+ <p className="mt-1.5 text-[11px] text-dpe-ink-400">
1522
1522
  {lines.length > 1
1523
1523
  ? t("detailPage.properties.textGifGroupHint", {
1524
1524
  defaultValue:
@@ -1646,7 +1646,7 @@ const CountUpGifSection = observer(function CountUpGifSection({
1646
1646
  type="button"
1647
1647
  onClick={run}
1648
1648
  disabled={busy}
1649
- className="inline-flex w-full items-center justify-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
1649
+ className="inline-flex w-full items-center justify-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-sm font-dpe-medium text-dpe-on-accent transition hover:bg-dpe-ink-700 disabled:cursor-not-allowed disabled:opacity-50"
1650
1650
  >
1651
1651
  <Film size={14} />
1652
1652
  {busy
@@ -1657,9 +1657,9 @@ const CountUpGifSection = observer(function CountUpGifSection({
1657
1657
  {creditCost ? ` · ${creditCost}` : ""}
1658
1658
  </button>
1659
1659
  {error ? (
1660
- <p className="mt-1.5 text-[11px] text-red-500">{error}</p>
1660
+ <p className="mt-1.5 text-[11px] text-dpe-danger-500">{error}</p>
1661
1661
  ) : (
1662
- <p className="mt-1.5 text-[11px] text-neutral-400">
1662
+ <p className="mt-1.5 text-[11px] text-dpe-ink-400">
1663
1663
  {t("detailPage.properties.countUpGifHint", {
1664
1664
  defaultValue:
1665
1665
  "0에서 {{target}}까지 오른 뒤 2초 멈췄다 반복해요. 이 자리에 그대로 들어갑니다.",
@@ -1744,7 +1744,7 @@ const CellGridGifSection = observer(function CellGridGifSection({
1744
1744
  >
1745
1745
  <div className="grid grid-cols-2 gap-2">
1746
1746
  <label className="flex flex-col gap-1">
1747
- <span className="text-[11px] text-neutral-500">
1747
+ <span className="text-[11px] text-dpe-ink-500">
1748
1748
  {t("detailPage.properties.cellGridRows", {
1749
1749
  defaultValue: "행별 채울 칸",
1750
1750
  })}
@@ -1753,11 +1753,11 @@ const CellGridGifSection = observer(function CellGridGifSection({
1753
1753
  value={rows}
1754
1754
  onChange={(e) => setRows(e.target.value)}
1755
1755
  placeholder="6,4,2"
1756
- className="h-8 rounded-md border border-neutral-200 px-2 text-sm"
1756
+ className="h-8 rounded-dpe-md border border-dpe-ink-200 px-2 text-sm"
1757
1757
  />
1758
1758
  </label>
1759
1759
  <label className="flex flex-col gap-1">
1760
- <span className="text-[11px] text-neutral-500">
1760
+ <span className="text-[11px] text-dpe-ink-500">
1761
1761
  {t("detailPage.properties.cellGridCols", { defaultValue: "열 수" })}
1762
1762
  </span>
1763
1763
  <input
@@ -1766,19 +1766,19 @@ const CellGridGifSection = observer(function CellGridGifSection({
1766
1766
  max={40}
1767
1767
  value={cols}
1768
1768
  onChange={(e) => setCols(Math.max(1, Number(e.target.value) || 1))}
1769
- className="h-8 rounded-md border border-neutral-200 px-2 text-sm"
1769
+ className="h-8 rounded-dpe-md border border-dpe-ink-200 px-2 text-sm"
1770
1770
  />
1771
1771
  </label>
1772
1772
  </div>
1773
1773
  <div className="mt-2 grid grid-cols-[1fr_auto_auto] items-end gap-2">
1774
1774
  <label className="flex flex-col gap-1">
1775
- <span className="text-[11px] text-neutral-500">
1775
+ <span className="text-[11px] text-dpe-ink-500">
1776
1776
  {t("detailPage.properties.cellGridShape", { defaultValue: "모양" })}
1777
1777
  </span>
1778
1778
  <select
1779
1779
  value={shape}
1780
1780
  onChange={(e) => setShape(e.target.value)}
1781
- className="h-8 rounded-md border border-neutral-200 px-2 text-sm"
1781
+ className="h-8 rounded-dpe-md border border-dpe-ink-200 px-2 text-sm"
1782
1782
  >
1783
1783
  {CELL_SHAPES.map((option) => (
1784
1784
  <option key={option.id} value={option.id}>
@@ -1794,7 +1794,7 @@ const CellGridGifSection = observer(function CellGridGifSection({
1794
1794
  type="button"
1795
1795
  onClick={run}
1796
1796
  disabled={busy || invalid}
1797
- className="mt-2 inline-flex w-full items-center justify-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-neutral-700 disabled:cursor-not-allowed disabled:opacity-50"
1797
+ className="mt-2 inline-flex w-full items-center justify-center gap-1.5 rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-sm font-dpe-medium text-dpe-on-accent transition hover:bg-dpe-ink-700 disabled:cursor-not-allowed disabled:opacity-50"
1798
1798
  >
1799
1799
  <Film size={14} />
1800
1800
  {busy
@@ -1804,7 +1804,7 @@ const CellGridGifSection = observer(function CellGridGifSection({
1804
1804
  })}
1805
1805
  {creditCost ? ` · ${creditCost}` : ""}
1806
1806
  </button>
1807
- <p className="mt-1.5 text-[11px] text-neutral-400">
1807
+ <p className="mt-1.5 text-[11px] text-dpe-ink-400">
1808
1808
  {error ??
1809
1809
  (invalid
1810
1810
  ? t("detailPage.properties.cellGridInvalid", {
@@ -1929,9 +1929,9 @@ const ImageInspector = observer(function ImageInspector({
1929
1929
  max={200}
1930
1930
  value={radius}
1931
1931
  onChange={(e) => setAll(els, { cornerRadius: Number(e.target.value) })}
1932
- className="min-w-0 flex-1 accent-neutral-900"
1932
+ className="min-w-0 flex-1 accent-dpe-ink-900"
1933
1933
  />
1934
- <span className="w-12 text-right text-sm tabular-nums text-neutral-700">
1934
+ <span className="w-12 text-right text-sm tabular-nums text-dpe-ink-700">
1935
1935
  {radius}px
1936
1936
  </span>
1937
1937
  </div>
@@ -1959,9 +1959,9 @@ const ImageInspector = observer(function ImageInspector({
1959
1959
  {/* AI 이미지 편집 — 선택된 이미지를 프롬프트로 실시간 재생성해 src를 교체한다.
1960
1960
  비싸므로 크레딧을 과금하며, 생성 ID가 있을 때만 노출(픽스처는 자동 생략). */}
1961
1961
  {single && generatedId ? (
1962
- <section className="border-t border-neutral-200">
1963
- <h4 className="flex items-center gap-1.5 px-4 pt-3 text-[11px] font-semibold uppercase tracking-[0.06em] text-neutral-400">
1964
- <Sparkles size={13} className="text-fuchsia-500" />
1962
+ <section className="border-t border-dpe-ink-200">
1963
+ <h4 className="flex items-center gap-1.5 px-4 pt-3 text-[11px] font-dpe-semibold uppercase tracking-[0.06em] text-dpe-ink-400">
1964
+ <Sparkles size={13} className="text-dpe-ai" />
1965
1965
  {isGif
1966
1966
  ? t("detailPage.properties.aiGifEdit")
1967
1967
  : t("detailPage.properties.aiImageEdit")}
@@ -2087,7 +2087,7 @@ const SvgColorSection = observer(function SvgColorSection({
2087
2087
  <button
2088
2088
  type="button"
2089
2089
  onClick={() => el.set({ colorsReplace: {} })}
2090
- className="mt-2 w-full rounded-md border border-neutral-200 py-1.5 text-[11px] font-medium text-neutral-500 hover:border-neutral-400 hover:bg-neutral-50"
2090
+ className="mt-2 w-full rounded-dpe-md border border-dpe-ink-200 py-1.5 text-[11px] font-dpe-medium text-dpe-ink-500 hover:border-dpe-ink-400 hover:bg-dpe-ink-50"
2091
2091
  >
2092
2092
  {t("detailPage.properties.shapeColorsReset")}
2093
2093
  </button>
@@ -2137,11 +2137,11 @@ const SvgInspector = observer(function SvgInspector({
2137
2137
  />
2138
2138
  ) : null}
2139
2139
  {single && currentSvg ? (
2140
- <div className="border-t border-neutral-200 px-4 py-2">
2140
+ <div className="border-t border-dpe-ink-200 px-4 py-2">
2141
2141
  <button
2142
2142
  type="button"
2143
2143
  onClick={() => void saveShapeToMyShapes(host, currentSvg, "manual_save", t)}
2144
- className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-neutral-200 py-2 text-xs font-medium text-neutral-600 hover:border-neutral-400 hover:bg-neutral-50"
2144
+ className="flex w-full items-center justify-center gap-1.5 rounded-dpe-lg border border-dpe-ink-200 py-2 text-xs font-dpe-medium text-dpe-ink-600 hover:border-dpe-ink-400 hover:bg-dpe-ink-50"
2145
2145
  >
2146
2146
  <Shapes size={13} />
2147
2147
  {t("detailPage.properties.saveToMyShapes")}
@@ -2149,8 +2149,8 @@ const SvgInspector = observer(function SvgInspector({
2149
2149
  </div>
2150
2150
  ) : null}
2151
2151
  {single && generatedId && currentSvg ? (
2152
- <section className="border-t border-neutral-200 px-4 py-3">
2153
- <h4 className="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-neutral-400">
2152
+ <section className="border-t border-dpe-ink-200 px-4 py-3">
2153
+ <h4 className="mb-2 flex items-center gap-1.5 text-[11px] font-dpe-semibold uppercase tracking-[0.06em] text-dpe-ink-400">
2154
2154
  <Sparkles size={13} className="text-primary" />
2155
2155
  {t("detailPage.properties.aiShapeEdit")}
2156
2156
  </h4>
@@ -2273,12 +2273,12 @@ const GroupInspector = observer(function GroupInspector({
2273
2273
  />
2274
2274
  ) : null}
2275
2275
  {generatedId && items.length > 0 ? (
2276
- <section className="border-t border-neutral-200 px-4 py-3">
2277
- <h4 className="mb-1 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-neutral-400">
2276
+ <section className="border-t border-dpe-ink-200 px-4 py-3">
2277
+ <h4 className="mb-1 flex items-center gap-1.5 text-[11px] font-dpe-semibold uppercase tracking-[0.06em] text-dpe-ink-400">
2278
2278
  <Sparkles size={13} className="text-primary" />
2279
2279
  그룹 편집
2280
2280
  </h4>
2281
- <p className="mb-2 text-xs text-neutral-400">{desc}</p>
2281
+ <p className="mb-2 text-xs text-dpe-ink-400">{desc}</p>
2282
2282
  <GroupPromptEditPanel
2283
2283
  generatedId={generatedId}
2284
2284
  items={items}
@@ -2329,9 +2329,9 @@ const FigureInspector = observer(function FigureInspector({
2329
2329
  max={200}
2330
2330
  value={radius}
2331
2331
  onChange={(e) => setAll(els, { cornerRadius: Number(e.target.value) })}
2332
- className="min-w-0 flex-1 accent-neutral-900"
2332
+ className="min-w-0 flex-1 accent-dpe-ink-900"
2333
2333
  />
2334
- <span className="w-12 text-right text-sm tabular-nums text-neutral-700">
2334
+ <span className="w-12 text-right text-sm tabular-nums text-dpe-ink-700">
2335
2335
  {radius}px
2336
2336
  </span>
2337
2337
  </div>
@@ -2385,12 +2385,12 @@ const PageHeightSection = observer(function PageHeightSection({
2385
2385
  type="button"
2386
2386
  disabled={contentBottom <= 0 || contentBottom === height}
2387
2387
  onClick={() => applySectionHeight(page, contentBottom)}
2388
- className="h-9 shrink-0 rounded-md border border-neutral-200 bg-white px-2.5 text-xs font-semibold text-neutral-700 hover:bg-neutral-50 disabled:cursor-not-allowed disabled:text-neutral-300"
2388
+ className="h-9 shrink-0 rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface px-2.5 text-xs font-dpe-semibold text-dpe-ink-700 hover:bg-dpe-ink-50 disabled:cursor-not-allowed disabled:text-dpe-ink-300"
2389
2389
  >
2390
2390
  {t("detailPage.properties.pageHeightFit")}
2391
2391
  </button>
2392
2392
  </div>
2393
- <p className="mt-2 text-xs text-neutral-400">
2393
+ <p className="mt-2 text-xs text-dpe-ink-400">
2394
2394
  {overflow
2395
2395
  ? t("detailPage.properties.pageHeightOverflow", { px: contentBottom })
2396
2396
  : t("detailPage.properties.pageHeightHint")}
@@ -2418,7 +2418,7 @@ const PageInspector = observer(function PageInspector({
2418
2418
  <PageHeightSection page={page} />
2419
2419
  <Section title={t("detailPage.properties.pageBackground")}>
2420
2420
  <FillControl value={bg} onChange={(c) => page?.set?.({ background: c })} />
2421
- <p className="mt-2 text-xs text-neutral-400">
2421
+ <p className="mt-2 text-xs text-dpe-ink-400">
2422
2422
  {t("detailPage.properties.pageBackgroundHint")}
2423
2423
  </p>
2424
2424
  </Section>
@@ -2466,7 +2466,7 @@ function InspectorHeader({ els }: { els: ElementLike[] }) {
2466
2466
  label = t("detailPage.properties.selectionCount", { count: els.length });
2467
2467
  }
2468
2468
  return (
2469
- <div className="flex items-center gap-2 border-b border-neutral-200 px-4 py-3 text-sm font-semibold text-neutral-950">
2469
+ <div className="flex items-center gap-2 border-b border-dpe-ink-200 px-4 py-3 text-sm font-dpe-semibold text-dpe-ink-950">
2470
2470
  {icon}
2471
2471
  {label}
2472
2472
  </div>
@@ -43,7 +43,7 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
43
43
 
44
44
  return (
45
45
  <div className="flex h-full flex-col gap-3 overflow-y-auto p-3">
46
- <div role="group" aria-label={t("detailPage.qr.kindLabel")} className="flex rounded-md border border-neutral-200 p-0.5">
46
+ <div role="group" aria-label={t("detailPage.qr.kindLabel")} className="flex rounded-dpe-md border border-dpe-ink-200 p-0.5">
47
47
  {(["qr", "ean13"] as const).map((option) => {
48
48
  const active = option === kind;
49
49
  return (
@@ -52,8 +52,8 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
52
52
  type="button"
53
53
  aria-pressed={active}
54
54
  onClick={() => setKind(option)}
55
- className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition-colors ${
56
- active ? "bg-neutral-900 text-white" : "text-neutral-500 hover:bg-neutral-100"
55
+ className={`flex-1 rounded px-2 py-1 text-[11px] font-dpe-medium transition-colors ${
56
+ active ? "bg-dpe-ink-900 text-dpe-on-accent" : "text-dpe-ink-500 hover:bg-dpe-ink-100"
57
57
  }`}
58
58
  >
59
59
  {t(option === "qr" ? "detailPage.qr.kindQr" : "detailPage.qr.kindEan")}
@@ -62,7 +62,7 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
62
62
  })}
63
63
  </div>
64
64
 
65
- <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
65
+ <label className="grid gap-1 text-[11px] font-dpe-medium text-dpe-ink-500">
66
66
  {t(kind === "qr" ? "detailPage.qr.valueLabel" : "detailPage.qr.eanLabel")}
67
67
  <input
68
68
  type="text"
@@ -72,28 +72,28 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
72
72
  kind === "qr" ? "detailPage.qr.valuePlaceholder" : "detailPage.qr.eanPlaceholder",
73
73
  )}
74
74
  inputMode={kind === "ean13" ? "numeric" : "text"}
75
- className="rounded-lg border border-neutral-200 px-2.5 py-2 text-sm placeholder:text-neutral-400 focus:border-neutral-400 focus:outline-none"
75
+ className="rounded-dpe-lg border border-dpe-ink-200 px-2.5 py-2 text-sm placeholder:text-dpe-ink-400 focus:border-dpe-ink-400 focus:outline-none"
76
76
  />
77
77
  </label>
78
78
 
79
79
  {invalid ? (
80
- <p className="text-xs text-red-600">
80
+ <p className="text-xs text-dpe-danger-600">
81
81
  {t(kind === "qr" ? "detailPage.qr.qrInvalid" : "detailPage.qr.eanInvalid")}
82
82
  </p>
83
83
  ) : null}
84
84
 
85
85
  <div className="grid grid-cols-2 gap-2">
86
- <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
86
+ <label className="grid gap-1 text-[11px] font-dpe-medium text-dpe-ink-500">
87
87
  {t("detailPage.qr.foreground")}
88
88
  <ColorInput value={foreground} onChange={setForeground} />
89
89
  </label>
90
- <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
90
+ <label className="grid gap-1 text-[11px] font-dpe-medium text-dpe-ink-500">
91
91
  {t("detailPage.qr.background")}
92
92
  <ColorInput value={background} onChange={setBackground} />
93
93
  </label>
94
94
  </div>
95
95
 
96
- <div className="flex min-h-24 items-center justify-center rounded-lg border border-dashed border-neutral-200 p-3">
96
+ <div className="flex min-h-24 items-center justify-center rounded-dpe-lg border border-dashed border-dpe-ink-200 p-3">
97
97
  {built ? (
98
98
  /* eslint-disable-next-line @next/next/no-img-element */
99
99
  <img
@@ -102,13 +102,13 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
102
102
  className="max-h-40 max-w-full object-contain"
103
103
  />
104
104
  ) : (
105
- <p className="text-xs text-neutral-400">{t("detailPage.qr.previewEmpty")}</p>
105
+ <p className="text-xs text-dpe-ink-400">{t("detailPage.qr.previewEmpty")}</p>
106
106
  )}
107
107
  </div>
108
108
 
109
109
  {/* EAN-13은 12자리를 넣으면 체크디짓을 채워 준다. 무엇이 들어갔는지 보여 준다. */}
110
110
  {built?.value ? (
111
- <p className="text-center text-xs tabular-nums text-neutral-500">{built.value}</p>
111
+ <p className="text-center text-xs tabular-nums text-dpe-ink-500">{built.value}</p>
112
112
  ) : null}
113
113
 
114
114
  <button
@@ -124,12 +124,12 @@ export function DetailPageQrPanel({ store }: { store: unknown }) {
124
124
  label: trimmed,
125
125
  });
126
126
  }}
127
- className="rounded-lg bg-neutral-900 px-3 py-2 text-sm font-semibold text-white hover:bg-neutral-800 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-400"
127
+ className="rounded-dpe-lg bg-dpe-ink-900 px-3 py-2 text-sm font-dpe-semibold text-dpe-on-accent hover:bg-dpe-ink-800 disabled:cursor-not-allowed disabled:bg-dpe-ink-200 disabled:text-dpe-ink-400"
128
128
  >
129
129
  {t("detailPage.qr.insert")}
130
130
  </button>
131
131
 
132
- <p className="mt-auto px-1 text-[11px] leading-relaxed text-neutral-500">
132
+ <p className="mt-auto px-1 text-[11px] leading-relaxed text-dpe-ink-500">
133
133
  {t("detailPage.qr.hint")}
134
134
  </p>
135
135
  </div>
@@ -99,7 +99,7 @@ export function DetailPageReferencesPanel({
99
99
  type="button"
100
100
  disabled={!activeBrandId || saveMutation.isPending}
101
101
  onClick={() => saveMutation.mutate()}
102
- className="mb-3 flex h-16 w-full flex-col items-center justify-center gap-1 rounded-md border border-dashed border-neutral-300 text-neutral-400 transition-colors hover:border-neutral-400 hover:text-neutral-600 disabled:opacity-40"
102
+ className="mb-3 flex h-16 w-full flex-col items-center justify-center gap-1 rounded-dpe-md border border-dashed border-dpe-ink-300 text-dpe-ink-400 transition-colors hover:border-dpe-ink-400 hover:text-dpe-ink-600 disabled:opacity-40"
103
103
  >
104
104
  {saveMutation.isPending ? (
105
105
  <Loader2 aria-hidden="true" className="animate-spin" size={18} />
@@ -111,21 +111,21 @@ export function DetailPageReferencesPanel({
111
111
  ) : null}
112
112
 
113
113
  {error ? (
114
- <p className="mb-2 text-xs font-medium text-red-600">{error}</p>
114
+ <p className="mb-2 text-xs font-dpe-medium text-dpe-danger-600">{error}</p>
115
115
  ) : null}
116
116
 
117
117
  {brandsLoading || listQuery.isLoading ? (
118
- <div className="flex flex-1 items-center justify-center text-neutral-400">
118
+ <div className="flex flex-1 items-center justify-center text-dpe-ink-400">
119
119
  <Loader2 aria-hidden="true" className="animate-spin" size={22} />
120
120
  </div>
121
121
  ) : !activeBrand ? (
122
- <p className="text-xs text-neutral-400">브랜드를 먼저 골라 주세요.</p>
122
+ <p className="text-xs text-dpe-ink-400">브랜드를 먼저 골라 주세요.</p>
123
123
  ) : listQuery.error ? (
124
- <p className="text-xs font-medium text-red-600">
124
+ <p className="text-xs font-dpe-medium text-dpe-danger-600">
125
125
  레퍼런스를 불러오지 못했어요.
126
126
  </p>
127
127
  ) : groups.length === 0 ? (
128
- <div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-neutral-400">
128
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-dpe-ink-400">
129
129
  <ImageOff aria-hidden="true" size={22} />
130
130
  <p className="text-xs">
131
131
  아직 저장한 레퍼런스가 없어요. 마음에 드는 상세페이지를 저장해 두면 다음
@@ -136,15 +136,15 @@ export function DetailPageReferencesPanel({
136
136
  <div className="space-y-4 overflow-y-auto">
137
137
  {groups.map((group) => (
138
138
  <section key={group.key}>
139
- <h3 className="mb-1.5 truncate text-[11px] font-medium text-neutral-600">
139
+ <h3 className="mb-1.5 truncate text-[11px] font-dpe-medium text-dpe-ink-600">
140
140
  {group.name}
141
- <span className="ml-1 text-neutral-400">{group.items.length}장</span>
141
+ <span className="ml-1 text-dpe-ink-400">{group.items.length}장</span>
142
142
  </h3>
143
143
  <div className="grid grid-cols-2 gap-2">
144
144
  {group.items.map((item) => (
145
145
  <figure
146
146
  key={item.asset_id}
147
- className="overflow-hidden rounded-md border border-neutral-200"
147
+ className="overflow-hidden rounded-dpe-md border border-dpe-ink-200"
148
148
  title={item.screen_label || item.display_name}
149
149
  >
150
150
  {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -103,7 +103,7 @@ function makeIconTab(
103
103
  {dividerBefore ? (
104
104
  <span
105
105
  aria-hidden="true"
106
- className="mx-3 my-1.5 block border-t border-neutral-200"
106
+ className="mx-3 my-1.5 block border-t border-dpe-ink-200"
107
107
  />
108
108
  ) : null}
109
109
  <Tab name={label} {...props}>
@@ -76,7 +76,7 @@ export function DetailPageTablesPanel({ store }: { store: unknown }) {
76
76
 
77
77
  return (
78
78
  <div className="flex h-full flex-col overflow-y-auto p-3">
79
- <p className="mb-2 text-xs font-medium text-neutral-500">
79
+ <p className="mb-2 text-xs font-dpe-medium text-dpe-ink-500">
80
80
  {t("detailPage.table.panelTitle")}
81
81
  </p>
82
82
  <div className="grid grid-cols-1 gap-2">
@@ -86,7 +86,7 @@ export function DetailPageTablesPanel({ store }: { store: unknown }) {
86
86
  type="button"
87
87
  onClick={() => insert(preset)}
88
88
  title={t("detailPage.table.insertHint")}
89
- className="flex flex-col items-center gap-1.5 rounded-lg border border-neutral-200 p-2 hover:border-neutral-400"
89
+ className="flex flex-col items-center gap-1.5 rounded-dpe-lg border border-dpe-ink-200 p-2 hover:border-dpe-ink-400"
90
90
  >
91
91
  {/* eslint-disable-next-line @next/next/no-img-element */}
92
92
  <img
@@ -94,11 +94,11 @@ export function DetailPageTablesPanel({ store }: { store: unknown }) {
94
94
  alt={t(preset.labelKey)}
95
95
  className="h-16 w-full object-contain"
96
96
  />
97
- <span className="text-[11px] text-neutral-500">{t(preset.labelKey)}</span>
97
+ <span className="text-[11px] text-dpe-ink-500">{t(preset.labelKey)}</span>
98
98
  </button>
99
99
  ))}
100
100
  </div>
101
- <p className="mt-3 text-[11px] leading-relaxed text-neutral-400">
101
+ <p className="mt-3 text-[11px] leading-relaxed text-dpe-ink-400">
102
102
  {t("detailPage.table.panelHint")}
103
103
  </p>
104
104
  </div>
@@ -135,7 +135,7 @@ export function DetailPageTextPanel({ store }: { store: unknown }) {
135
135
  fontWeight: preset.fontWeight,
136
136
  })
137
137
  }
138
- className="rounded-lg border border-neutral-200 px-3 py-3 text-left text-neutral-900 hover:border-neutral-400"
138
+ className="rounded-dpe-lg border border-dpe-ink-200 px-3 py-3 text-left text-dpe-ink-900 hover:border-dpe-ink-400"
139
139
  style={{
140
140
  fontSize: preset.previewSize,
141
141
  fontWeight: preset.fontWeight,
@@ -146,7 +146,7 @@ export function DetailPageTextPanel({ store }: { store: unknown }) {
146
146
  );
147
147
  })}
148
148
 
149
- <p className="mt-2 px-1 text-xs font-medium text-neutral-500">
149
+ <p className="mt-2 px-1 text-xs font-dpe-medium text-dpe-ink-500">
150
150
  {t("detailPage.textPresets.title")}
151
151
  </p>
152
152
  {TEXT_PRESETS.map((preset) => {
@@ -164,14 +164,14 @@ export function DetailPageTextPanel({ store }: { store: unknown }) {
164
164
  name: t(`detailPage.textPresets.${preset.key}.name`),
165
165
  })
166
166
  }
167
- className="rounded-lg border border-neutral-200 px-3 py-3 hover:border-neutral-400"
167
+ className="rounded-dpe-lg border border-dpe-ink-200 px-3 py-3 hover:border-dpe-ink-400"
168
168
  >
169
169
  <PresetPreview preset={preset} label={label} />
170
170
  </button>
171
171
  );
172
172
  })}
173
173
 
174
- <p className="mt-1 px-1 text-xs leading-relaxed text-neutral-500">
174
+ <p className="mt-1 px-1 text-xs leading-relaxed text-dpe-ink-500">
175
175
  {t("detailPage.text.hint")}
176
176
  </p>
177
177
  </div>
@@ -106,7 +106,7 @@ export function EditUsageBadge({
106
106
  (remaining === 0
107
107
  ? "text-destructive"
108
108
  : warn
109
- ? "text-amber-500"
109
+ ? "text-dpe-warn-500"
110
110
  : "text-muted-foreground")
111
111
  }
112
112
  title={t("detailPage.editQuota.remainingTitle", { remaining })}
@@ -130,7 +130,7 @@ export function EditQuotaBlock({
130
130
  : t("detailPage.editQuota.textEdit");
131
131
  return (
132
132
  <div className="flex flex-col items-center gap-2 px-4 py-4 text-center">
133
- <p className="text-[13px] font-medium text-foreground">
133
+ <p className="text-[13px] font-dpe-medium text-foreground">
134
134
  {t("detailPage.editQuota.quotaExhausted", { label })}
135
135
  </p>
136
136
  <p className="text-[11px] leading-4 text-muted-foreground">
@@ -140,7 +140,7 @@ export function EditQuotaBlock({
140
140
  type="button"
141
141
  onClick={onBuyMore}
142
142
  disabled={!onBuyMore}
143
- className="mt-1 inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground disabled:opacity-40"
143
+ className="mt-1 inline-flex items-center gap-1.5 rounded-dpe-lg bg-primary px-3 py-1.5 text-xs font-dpe-medium text-primary-foreground disabled:opacity-40"
144
144
  >
145
145
  <Sparkles size={13} />
146
146
  {t("detailPage.editQuota.buyCredits")}