@leviosa-ai/detail-page-editor 0.12.0 → 0.12.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.
@@ -58,7 +58,8 @@ type ExportStoreLike = {
58
58
  };
59
59
 
60
60
  // 등록 플랫폼은 파일명 접미사에만 반영된다(상세페이지 폭은 750px 고정). 셀러가
61
- // 어디에 올릴지 고르며 맥락을 잡도록 노출한다.
61
+ // 어디에 올릴지 고르며 맥락을 잡도록 노출한다. 캐러셀은 인스타그램 한 곳으로만
62
+ // 나가므로 이 목록을 아예 안 띄운다 — `profile.registerPlatform` 이 그것을 정한다.
62
63
  const PLATFORMS = [
63
64
  { value: "naver", label: "네이버 스마트 스토어" },
64
65
  { value: "coupang", label: "쿠팡" },
@@ -220,7 +221,7 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
220
221
  setError(null);
221
222
  const mime = format === "png" ? "image/png" : "image/jpeg";
222
223
  const ext = format === "png" ? "png" : "jpg";
223
- const base = `${fileName}-${platform}`;
224
+ const base = profile.registerPlatform ? `${fileName}-${platform}` : fileName;
224
225
  try {
225
226
  if (format === "psd" || format === "svg" || format === "ai") {
226
227
  // 문서 JSON 기반 내보내기. ag-psd 포함 모듈은 이 시점에만 로드한다.
@@ -384,23 +385,25 @@ export const DetailPageDownloadDialog = observer(function DetailPageDownloadDial
384
385
  </Select>
385
386
  </div>
386
387
 
387
- <div className="space-y-1.5">
388
- <Label className="text-xs text-dpe-ink-500">
389
- {t("editor.registerPlatform")}
390
- </Label>
391
- <Select value={platform} onValueChange={setPlatform}>
392
- <SelectTrigger className="w-full">
393
- <SelectValue />
394
- </SelectTrigger>
395
- <SelectContent>
396
- {PLATFORMS.map((p) => (
397
- <SelectItem key={p.value} value={p.value}>
398
- {p.label}
399
- </SelectItem>
400
- ))}
401
- </SelectContent>
402
- </Select>
403
- </div>
388
+ {profile.registerPlatform && (
389
+ <div className="space-y-1.5">
390
+ <Label className="text-xs text-dpe-ink-500">
391
+ {t("editor.registerPlatform")}
392
+ </Label>
393
+ <Select value={platform} onValueChange={setPlatform}>
394
+ <SelectTrigger className="w-full">
395
+ <SelectValue />
396
+ </SelectTrigger>
397
+ <SelectContent>
398
+ {PLATFORMS.map((p) => (
399
+ <SelectItem key={p.value} value={p.value}>
400
+ {p.label}
401
+ </SelectItem>
402
+ ))}
403
+ </SelectContent>
404
+ </Select>
405
+ </div>
406
+ )}
404
407
 
405
408
  <div className="space-y-1.5">
406
409
  <Label className="text-xs text-dpe-ink-500">{t("editor.pageScope")}</Label>
@@ -90,7 +90,7 @@ export function EditUsageBadge({
90
90
  if (unlimited) {
91
91
  return (
92
92
  <span
93
- className="ml-auto text-[11px] tabular-nums text-muted-foreground"
93
+ className="ml-auto text-[11px] tabular-nums text-dpe-ink-500"
94
94
  title={t("detailPage.editQuota.unlimitedTitle")}
95
95
  >
96
96
  {t("detailPage.editQuota.usedUnlimited", { used })}
@@ -104,10 +104,10 @@ export function EditUsageBadge({
104
104
  className={
105
105
  "ml-auto text-[11px] tabular-nums " +
106
106
  (remaining === 0
107
- ? "text-destructive"
107
+ ? "text-dpe-danger-600"
108
108
  : warn
109
109
  ? "text-dpe-warn-500"
110
- : "text-muted-foreground")
110
+ : "text-dpe-ink-500")
111
111
  }
112
112
  title={t("detailPage.editQuota.remainingTitle", { remaining })}
113
113
  >
@@ -130,17 +130,17 @@ 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-dpe-medium text-foreground">
133
+ <p className="text-[13px] font-dpe-medium text-dpe-ink-900">
134
134
  {t("detailPage.editQuota.quotaExhausted", { label })}
135
135
  </p>
136
- <p className="text-[11px] leading-4 text-muted-foreground">
136
+ <p className="text-[11px] leading-4 text-dpe-ink-500">
137
137
  {t("detailPage.editQuota.quotaHint")}
138
138
  </p>
139
139
  <button
140
140
  type="button"
141
141
  onClick={onBuyMore}
142
142
  disabled={!onBuyMore}
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"
143
+ className="mt-1 inline-flex items-center gap-1.5 rounded-dpe-lg bg-dpe-ink-900 px-3 py-1.5 text-xs font-dpe-medium text-dpe-on-accent disabled:opacity-40"
144
144
  >
145
145
  <Sparkles size={13} />
146
146
  {t("detailPage.editQuota.buyCredits")}
@@ -38,18 +38,18 @@ export function progressPercent(progress: number): number {
38
38
 
39
39
  /**
40
40
  * 밝은 배경 패널(카피·SVG 프롬프트 편집)의 하단에 얹는 얇은 프로그레스 라인.
41
- * 테마 토큰(primary) 기반이라 라이트/다크 모두 자연스럽다. 부모는 `relative`여야
42
- * 하며, 이 요소는 컨테이너 하단에 절대배치된다.
41
+ * 색은 AI 표식 토큰(`dpe-ai`)이다 — 이 선이 도는 동안 일어나는 일이 생성이기
42
+ * 때문이다. 부모는 `relative`여야 하며, 이 요소는 컨테이너 하단에 절대배치된다.
43
43
  */
44
44
  export function GenerationProgressLine({ progress }: { progress: number }) {
45
45
  const pct = progressPercent(progress);
46
46
  return (
47
47
  <span
48
48
  aria-hidden="true"
49
- className="pointer-events-none absolute inset-x-0 bottom-0 h-[3px] bg-primary/15"
49
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-[3px] bg-dpe-ai/15"
50
50
  >
51
51
  <span
52
- className="block h-full bg-primary"
52
+ className="block h-full bg-dpe-ai"
53
53
  style={{ width: `${pct}%`, transition: "width 200ms ease-out" }}
54
54
  />
55
55
  </span>
@@ -106,10 +106,10 @@ export function GroupPromptEditPanel({
106
106
  const badgeLimit = hasText ? textLimit : svgLimit;
107
107
 
108
108
  return (
109
- <div className="flex flex-col overflow-hidden rounded-dpe-xl border border-border bg-card">
110
- <div className="flex items-center gap-1.5 border-b border-border px-3 py-2">
111
- <Sparkles size={13} className="text-primary" />
112
- <span className="text-xs font-dpe-medium text-foreground">프롬프트로 편집</span>
109
+ <div className="flex flex-col overflow-hidden rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface">
110
+ <div className="flex items-center gap-1.5 border-b border-dpe-ink-200 px-3 py-2">
111
+ <Sparkles size={13} className="text-dpe-ai" />
112
+ <span className="text-xs font-dpe-medium text-dpe-ink-900">프롬프트로 편집</span>
113
113
  {unlimited || badgeLimit > 0 ? (
114
114
  <EditUsageBadge used={badgeUsed} limit={badgeLimit} unlimited={unlimited} />
115
115
  ) : null}
@@ -132,13 +132,13 @@ export function GroupPromptEditPanel({
132
132
  busy ? "수정 중…" : '어떻게 바꿀까요? (예: "더 힘있게", "톤을 통일해서")'
133
133
  }
134
134
  disabled={busy}
135
- className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-foreground outline-none placeholder:text-muted-foreground/70 disabled:opacity-60"
135
+ className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-dpe-ink-900 outline-none placeholder:text-dpe-ink-400 disabled:opacity-60"
136
136
  />
137
137
  <button
138
138
  type="button"
139
139
  onClick={send}
140
140
  disabled={busy || !input.trim()}
141
- className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-primary text-primary-foreground disabled:opacity-40"
141
+ className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-dpe-ink-900 text-dpe-on-accent disabled:opacity-40"
142
142
  aria-label="프롬프트로 수정"
143
143
  >
144
144
  {busy ? (
@@ -97,10 +97,10 @@ export function PromptEditPanel({
97
97
  }, [api, toast, input, busy, blocked, generatedId, slotRole, currentText, maxLength, renderKind, onApplied, onUsage, t]);
98
98
 
99
99
  return (
100
- <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-border bg-card">
101
- <div className="flex items-center gap-1.5 border-b border-border px-3 py-2">
102
- <Sparkles size={13} className="text-primary" />
103
- <span className="text-xs font-dpe-medium text-foreground">{t("detailPage.promptEdit.header")}</span>
100
+ <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface">
101
+ <div className="flex items-center gap-1.5 border-b border-dpe-ink-200 px-3 py-2">
102
+ <Sparkles size={13} className="text-dpe-ai" />
103
+ <span className="text-xs font-dpe-medium text-dpe-ink-900">{t("detailPage.promptEdit.header")}</span>
104
104
  {unlimited || editLimit > 0 ? (
105
105
  <EditUsageBadge used={editsUsed} limit={editLimit} unlimited={unlimited} />
106
106
  ) : null}
@@ -123,13 +123,13 @@ export function PromptEditPanel({
123
123
  busy ? t("detailPage.promptEdit.editing") : t("detailPage.promptEdit.textPlaceholder")
124
124
  }
125
125
  disabled={busy}
126
- className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-foreground outline-none placeholder:text-muted-foreground/70 disabled:opacity-60"
126
+ className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-dpe-ink-900 outline-none placeholder:text-dpe-ink-400 disabled:opacity-60"
127
127
  />
128
128
  <button
129
129
  type="button"
130
130
  onClick={send}
131
131
  disabled={busy || !input.trim()}
132
- className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-primary text-primary-foreground disabled:opacity-40"
132
+ className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-dpe-ink-900 text-dpe-on-accent disabled:opacity-40"
133
133
  aria-label={t("detailPage.promptEdit.send")}
134
134
  >
135
135
  {busy ? (
@@ -122,10 +122,10 @@ export function SpecPromptEditPanel({
122
122
  ]);
123
123
 
124
124
  return (
125
- <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-border bg-card">
126
- <div className="flex items-center gap-1.5 border-b border-border px-3 py-2">
127
- <Sparkles size={13} className="text-primary" />
128
- <span className="text-xs font-dpe-medium text-foreground">
125
+ <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface">
126
+ <div className="flex items-center gap-1.5 border-b border-dpe-ink-200 px-3 py-2">
127
+ <Sparkles size={13} className="text-dpe-ai" />
128
+ <span className="text-xs font-dpe-medium text-dpe-ink-900">
129
129
  {t("detailPage.promptEdit.header")}
130
130
  </span>
131
131
  {unlimited || editLimit > 0 ? (
@@ -152,13 +152,13 @@ export function SpecPromptEditPanel({
152
152
  : t(`detailPage.specPromptEdit.placeholder.${specKind}`)
153
153
  }
154
154
  disabled={busy}
155
- className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-foreground outline-none placeholder:text-muted-foreground/70 disabled:opacity-60"
155
+ className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-dpe-ink-900 outline-none placeholder:text-dpe-ink-400 disabled:opacity-60"
156
156
  />
157
157
  <button
158
158
  type="button"
159
159
  onClick={send}
160
160
  disabled={busy || !input.trim()}
161
- className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-primary text-primary-foreground disabled:opacity-40"
161
+ className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-dpe-ink-900 text-dpe-on-accent disabled:opacity-40"
162
162
  aria-label={t("detailPage.promptEdit.send")}
163
163
  >
164
164
  {busy ? (
@@ -170,7 +170,7 @@ export function SpecPromptEditPanel({
170
170
  </div>
171
171
  )}
172
172
  {busy ? <GenerationProgressLine progress={progress} /> : null}
173
- <p className="px-3 pb-2 text-[11px] leading-relaxed text-muted-foreground">
173
+ <p className="px-3 pb-2 text-[11px] leading-relaxed text-dpe-ink-500">
174
174
  {t("detailPage.specPromptEdit.hint")}
175
175
  </p>
176
176
  </div>
@@ -90,10 +90,10 @@ export function SvgPromptEditPanel({
90
90
  }, [api, toast, input, busy, blocked, generatedId, slotRole, currentSvg, onApplied, onUsage, t]);
91
91
 
92
92
  return (
93
- <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-border bg-card">
94
- <div className="flex items-center gap-1.5 border-b border-border px-3 py-2">
95
- <Sparkles size={13} className="text-primary" />
96
- <span className="text-xs font-dpe-medium text-foreground">{t("detailPage.promptEdit.header")}</span>
93
+ <div className="relative flex flex-col overflow-hidden rounded-dpe-xl border border-dpe-ink-200 bg-dpe-surface">
94
+ <div className="flex items-center gap-1.5 border-b border-dpe-ink-200 px-3 py-2">
95
+ <Sparkles size={13} className="text-dpe-ai" />
96
+ <span className="text-xs font-dpe-medium text-dpe-ink-900">{t("detailPage.promptEdit.header")}</span>
97
97
  {unlimited || editLimit > 0 ? (
98
98
  <EditUsageBadge used={editsUsed} limit={editLimit} unlimited={unlimited} />
99
99
  ) : null}
@@ -116,13 +116,13 @@ export function SvgPromptEditPanel({
116
116
  busy ? t("detailPage.promptEdit.editing") : t("detailPage.svgPromptEdit.placeholder")
117
117
  }
118
118
  disabled={busy}
119
- className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-foreground outline-none placeholder:text-muted-foreground/70 disabled:opacity-60"
119
+ className="max-h-32 flex-1 resize-none bg-transparent px-2 py-1.5 text-[13px] leading-5 text-dpe-ink-900 outline-none placeholder:text-dpe-ink-400 disabled:opacity-60"
120
120
  />
121
121
  <button
122
122
  type="button"
123
123
  onClick={send}
124
124
  disabled={busy || !input.trim()}
125
- className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-primary text-primary-foreground disabled:opacity-40"
125
+ className="grid h-8 w-8 shrink-0 place-items-center rounded-dpe-lg bg-dpe-ink-900 text-dpe-on-accent disabled:opacity-40"
126
126
  aria-label={t("detailPage.promptEdit.send")}
127
127
  >
128
128
  {busy ? (
@@ -1,3 +1,16 @@
1
+ /**
2
+ * 편집기 버튼.
3
+ *
4
+ * 색은 **편집기 토큰(`dpe-*`)만** 쓴다. `bg-primary` · `text-primary-foreground` 는
5
+ * 소비자 앱의 CSS 변수를 읽는 이름이고, leviosa-agency 는 `--color-primary-foreground`
6
+ * 를 안 둔다. 그래서 글자색 클래스가 한 줄도 안 구워졌고, 글자가 본문
7
+ * (`--color-fg: #1a1a1a`)을 물려받아 호출부가 덮은 배경(`bg-dpe-ink-900`, 같은 앱에서
8
+ * `#1a1a1a`)과 정확히 같은 값이 됐다 — 다운로드 팝오버의 저장 버튼이 검은 판때기로
9
+ * 보인 이유다.
10
+ *
11
+ * 테두리도 같다. 색 없는 `border` 는 currentColor 라, 글자색이 어두우면 테두리가
12
+ * 통째로 먹으로 나온다. 색을 쓰는 자리마다 토큰을 적는다.
13
+ */
1
14
  import * as React from "react"
2
15
  import { cva, type VariantProps } from "class-variance-authority"
3
16
  import { Slot } from "radix-ui"
@@ -13,7 +26,7 @@ const buttonVariants = cva(
13
26
  destructive:
14
27
  "bg-dpe-danger-500 text-dpe-on-accent hover:bg-dpe-danger-500/90 focus-visible:ring-dpe-danger-500/20 dark:focus-visible:ring-dpe-danger-500/40 dark:bg-dpe-danger-500/60",
15
28
  outline:
16
- "border bg-dpe-surface shadow-xs hover:bg-dpe-ink-100 hover:text-dpe-ink-900 dark:bg-dpe-ink-100/30 dark:border-dpe-ink-200 dark:hover:bg-dpe-ink-100/50",
29
+ "border border-dpe-ink-200 bg-dpe-surface text-dpe-ink-900 shadow-xs hover:bg-dpe-ink-100 dark:bg-dpe-ink-100/30 dark:hover:bg-dpe-ink-100/50",
17
30
  secondary:
18
31
  "bg-dpe-ink-100 text-dpe-ink-900 hover:bg-dpe-ink-100/80",
19
32
  ghost:
@@ -1,5 +1,10 @@
1
1
  "use client";
2
2
 
3
+ /**
4
+ * 편집기 모달. 포털이 body 로 나가므로 `data-dpe-root` 를 다시 달고(아래), 판의
5
+ * 글자색과 테두리색은 물려받지 않고 적는다 — 색 없는 `border` 는 currentColor 다.
6
+ * 이유는 select.tsx 주석에 있다.
7
+ */
3
8
  import * as React from "react";
4
9
  import { XIcon } from "lucide-react";
5
10
  import { Dialog as DialogPrimitive } from "radix-ui";
@@ -65,7 +70,7 @@ function DialogContent({
65
70
  data-slot="dialog-content"
66
71
  data-dpe-root=""
67
72
  className={cn(
68
- "bg-dpe-surface data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[115] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-dpe-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
73
+ "bg-dpe-surface text-dpe-ink-900 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[115] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-dpe-lg border border-dpe-ink-200 p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
69
74
  className,
70
75
  )}
71
76
  onOpenAutoFocus={(event) => {
@@ -1,5 +1,18 @@
1
1
  "use client"
2
2
 
3
+ /**
4
+ * Radix Select 위에 얹은 드롭다운.
5
+ *
6
+ * 색은 **편집기 토큰(`dpe-*`)만** 쓴다. shadcn 원본이 달고 온 `bg-popover` ·
7
+ * `text-popover-foreground` · `border-input` · `bg-accent` · `text-muted-foreground`
8
+ * 는 전부 소비자 앱의 팔레트를 읽는 이름이라, 이 패키지가 그 값을 정할 방법이 없다.
9
+ * leviosa-agency 는 `--color-popover` 를 아예 안 두므로 그 유틸리티가 한 줄도 안
10
+ * 구워졌고, 목록 판이 **투명해져** 뒤의 라벨과 글자가 겹쳐 보였다.
11
+ *
12
+ * 글자색은 물려받지 않고 판마다 적는다(`text-dpe-ink-900`). 이 버그가 아픈 이유가
13
+ * 정확히 "안 적으면 바깥 것을 물려받는다" 였다 — 판은 토큰으로 칠하면서 글자를
14
+ * 물려받게 두면 갈아입힌 앱에서 같은 자리에 같은 일이 다시 난다.
15
+ */
3
16
  import * as React from "react"
4
17
  import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
5
18
  import { Select as SelectPrimitive } from "radix-ui"
@@ -37,7 +50,7 @@ function SelectTrigger({
37
50
  data-slot="select-trigger"
38
51
  data-size={size}
39
52
  className={cn(
40
- "border-dpe-ink-200 data-[placeholder]:text-dpe-ink-500 [&_svg:not([class*='text-'])]:text-dpe-ink-500 focus-visible:border-dpe-ink-400 focus-visible:ring-dpe-ink-300/50 aria-invalid:ring-dpe-danger-500/20 dark:aria-invalid:ring-dpe-danger-500/40 aria-invalid:border-dpe-danger-500 dark:bg-dpe-ink-100/30 dark:hover:bg-dpe-ink-100/50 flex w-fit items-center justify-between gap-2 rounded-dpe-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
53
+ "border-dpe-ink-200 text-dpe-ink-900 data-[placeholder]:text-dpe-ink-400 [&_svg:not([class*='text-'])]:text-dpe-ink-500 focus-visible:border-dpe-ink-400 focus-visible:ring-dpe-ink-300/50 aria-invalid:ring-dpe-danger-500/20 dark:aria-invalid:ring-dpe-danger-500/40 aria-invalid:border-dpe-danger-500 dark:bg-dpe-ink-100/30 dark:hover:bg-dpe-ink-100/50 flex w-fit items-center justify-between gap-2 rounded-dpe-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
41
54
  className
42
55
  )}
43
56
  {...props}
@@ -114,7 +127,7 @@ function SelectItem({
114
127
  <SelectPrimitive.Item
115
128
  data-slot="select-item"
116
129
  className={cn(
117
- "focus:bg-dpe-ink-100 focus:text-dpe-ink-900 [&_svg:not([class*='text-'])]:text-dpe-ink-500 relative flex w-full cursor-default items-center gap-2 rounded-dpe-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
130
+ "text-dpe-ink-900 focus:bg-dpe-ink-100 [&_svg:not([class*='text-'])]:text-dpe-ink-500 relative flex w-full cursor-default items-center gap-2 rounded-dpe-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
118
131
  className
119
132
  )}
120
133
  {...props}
@@ -1,5 +1,10 @@
1
1
  "use client"
2
2
 
3
+ /**
4
+ * 해상도 배율 같은 값 슬라이더. 궤도·채움·손잡이 전부 편집기 토큰이다 — 원본의
5
+ * `bg-muted` · `bg-primary` 는 소비자 앱의 이름이라, 그 앱이 안 두면 채운 구간이
6
+ * 통째로 안 보인다(select.tsx 주석 참고).
7
+ */
3
8
  import * as React from "react"
4
9
  import { Slider as SliderPrimitive } from "radix-ui"
5
10
 
@@ -1,5 +1,12 @@
1
1
  "use client"
2
2
 
3
+ /**
4
+ * 켬/끔 스위치. 켠 색은 먹(`dpe-ink-900`), 끈 색은 `dpe-ink-300` 이다.
5
+ *
6
+ * 끈 색이 `ink-100` 이면 흰 패널(`dpe-surface`) 위에서 궤도가 거의 안 보인다 —
7
+ * 손잡이도 흰색이고 테두리는 투명이라, 꺼진 스위치가 통째로 사라진다. 한 단계
8
+ * 내려서 꺼짐이 꺼짐으로 읽히게 한다.
9
+ */
3
10
  import * as React from "react"
4
11
  import { Switch as SwitchPrimitive } from "radix-ui"
5
12
 
@@ -17,7 +24,7 @@ function Switch({
17
24
  data-slot="switch"
18
25
  data-size={size}
19
26
  className={cn(
20
- "peer data-[state=checked]:bg-dpe-ink-900 data-[state=unchecked]:bg-dpe-ink-100 focus-visible:border-dpe-ink-400 focus-visible:ring-dpe-ink-300/50 dark:data-[state=unchecked]:bg-dpe-ink-100/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
27
+ "peer data-[state=checked]:bg-dpe-ink-900 data-[state=unchecked]:bg-dpe-ink-300 focus-visible:border-dpe-ink-400 focus-visible:ring-dpe-ink-300/50 dark:data-[state=unchecked]:bg-dpe-ink-100/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
21
28
  className
22
29
  )}
23
30
  {...props}
@@ -1,5 +1,12 @@
1
1
  "use client"
2
2
 
3
+ /**
4
+ * 말풍선 도움말. 어두운 판(`dpe-ink-900`) 위 `dpe-on-accent` 글자다.
5
+ *
6
+ * `text-background` 는 소비자 앱의 이름이라, 그 앱이 안 두면 클래스가 아예 안 구워지고
7
+ * 글자색이 물려받은 값(먹)으로 남는다 — 어두운 판 위 어두운 글자. 판만 토큰으로
8
+ * 옮기고 글자를 안 옮기면 오히려 원본보다 안 보인다.
9
+ */
3
10
  import * as React from "react"
4
11
  import { Tooltip as TooltipPrimitive } from "radix-ui"
5
12
 
@@ -43,7 +50,7 @@ function TooltipContent({
43
50
  data-slot="tooltip-content"
44
51
  sideOffset={sideOffset}
45
52
  className={cn(
46
- "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
53
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-dpe-md bg-dpe-ink-900 px-3 py-1.5 text-xs text-balance text-dpe-on-accent fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
47
54
  className
48
55
  )}
49
56
  {...props}
@@ -10,6 +10,13 @@ export type DetailPageEditorProfile = {
10
10
  };
11
11
  maxPages: number;
12
12
  exports: readonly DetailPageEditorFormat[];
13
+ /**
14
+ * 다운로드 팝오버에 "등록 플랫폼"을 물어볼지. 그 답은 파일명 접미사에만 쓰이고,
15
+ * 셀러가 상품을 어디에 올릴지 고르며 맥락을 잡으라고 있는 것이다. 캐러셀은
16
+ * 인스타그램 한 곳으로 나가므로 물어볼 것이 없다 — 고를 수 없는 선택지를
17
+ * 남겨 두면 접미사만 거짓말을 한다(`-naver` 붙은 인스타 이미지).
18
+ */
19
+ registerPlatform: boolean;
13
20
  wording: "section" | "plate";
14
21
  };
15
22
 
@@ -17,13 +24,19 @@ const DETAIL_PAGE_PROFILE: DetailPageEditorProfile = {
17
24
  page: { width: "document", height: "auto", fixed: false },
18
25
  maxPages: Number.POSITIVE_INFINITY,
19
26
  exports: ["png", "jpeg", "psd", "ai", "svg"],
27
+ registerPlatform: true,
20
28
  wording: "section",
21
29
  };
22
30
 
23
31
  const CAROUSEL_PROFILE: DetailPageEditorProfile = {
24
32
  page: { width: 1080, height: 1350, fixed: true },
25
33
  maxPages: 10,
26
- exports: ["jpeg"],
34
+ // 상세페이지와 같은 다섯 가지다. 한때 JPG 하나로 좁혀 뒀던 것은 인스타그램이
35
+ // JPG 로 받는다는 이유였는데, 내보낸 파일이 곧장 업로드로만 가는 것이 아니다 —
36
+ // 투명 배경을 살리려면 PNG 가, 다른 도구로 넘겨 손보려면 PSD·AI·SVG 가 필요하다.
37
+ // 첫 항목이 기본값이므로 JPG 를 앞에 둬서 지금까지의 기본 동작은 그대로 둔다.
38
+ exports: ["jpeg", "png", "psd", "ai", "svg"],
39
+ registerPlatform: false,
27
40
  wording: "plate",
28
41
  };
29
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviosa-ai/detail-page-editor",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Leviosa detail-page editor shell. Source-only: consumers compile it through Next transpilePackages. Every app coupling (sourcing server, brand store, cache keys, toast) arrives through the DetailPageHost interface, and every URL it fetches comes from configureDetailPageEditor.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",