@leviosa-ai/detail-page-editor 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/cardnews/color-input.tsx +9 -9
- package/components/detail-page/ai-generate-panel.tsx +37 -37
- package/components/detail-page/annotation-canvas.tsx +9 -9
- package/components/detail-page/annotation-dialog.tsx +18 -18
- package/components/detail-page/canvas-context-menu.tsx +4 -4
- package/components/detail-page/chart-inspector.tsx +23 -23
- package/components/detail-page/design-reference-picker.tsx +22 -22
- package/components/detail-page/detail-page-basic-shapes.tsx +4 -4
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +10 -10
- package/components/detail-page/detail-page-brand-kit-panel.tsx +5 -5
- package/components/detail-page/detail-page-brand-panel-header.tsx +7 -4
- package/components/detail-page/detail-page-charts-panel.tsx +4 -4
- package/components/detail-page/detail-page-decorations-panel.tsx +5 -5
- package/components/detail-page/detail-page-download-dialog.tsx +19 -19
- package/components/detail-page/detail-page-editor.tsx +131 -76
- package/components/detail-page/detail-page-elements-panel.tsx +4 -4
- package/components/detail-page/detail-page-font-picker.tsx +14 -14
- package/components/detail-page/detail-page-history-buttons.tsx +3 -3
- package/components/detail-page/detail-page-host-context.tsx +63 -0
- package/components/detail-page/detail-page-icons-panel.tsx +13 -13
- package/components/detail-page/detail-page-konva-json-preview.tsx +9 -9
- package/components/detail-page/detail-page-layers-panel.tsx +14 -14
- package/components/detail-page/detail-page-my-images-panel.tsx +29 -14
- package/components/detail-page/detail-page-my-shapes-panel.tsx +8 -8
- package/components/detail-page/detail-page-page-toolbar.tsx +10 -10
- package/components/detail-page/detail-page-pages-panel.tsx +13 -13
- package/components/detail-page/detail-page-pages-timeline.tsx +4 -4
- package/components/detail-page/detail-page-photos-panel.tsx +13 -13
- package/components/detail-page/detail-page-properties-panel.tsx +55 -55
- package/components/detail-page/detail-page-qr-panel.tsx +13 -13
- package/components/detail-page/detail-page-references-panel.tsx +9 -9
- package/components/detail-page/detail-page-sidebar-sections.tsx +1 -1
- package/components/detail-page/detail-page-tables-panel.tsx +4 -4
- package/components/detail-page/detail-page-text-panel.tsx +4 -4
- package/components/detail-page/edit-quota-ui.tsx +3 -3
- package/components/detail-page/element-recents-strip.tsx +6 -6
- package/components/detail-page/fill-control.tsx +12 -12
- package/components/detail-page/find-replace-panel.tsx +8 -8
- package/components/detail-page/generation-progress-fill.tsx +4 -4
- package/components/detail-page/gif-effect-picker.tsx +9 -9
- package/components/detail-page/group-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/inspector-controls.tsx +9 -9
- package/components/detail-page/panel-search-input.tsx +2 -2
- package/components/detail-page/prompt-edit-panel.tsx +3 -3
- package/components/detail-page/section-reauthor-controller.tsx +2 -2
- package/components/detail-page/spec-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/svg-prompt-edit-panel.tsx +3 -3
- package/components/detail-page/table-inspector.tsx +24 -24
- package/components/ui/button.tsx +6 -6
- package/components/ui/command.tsx +5 -5
- package/components/ui/dialog.tsx +3 -3
- package/components/ui/label.tsx +1 -1
- package/components/ui/select.tsx +3 -3
- package/components/ui/slider.tsx +1 -1
- package/components/ui/tooltip.tsx +1 -1
- package/lib/detail-page/use-progressive-reveal.ts +82 -0
- package/lib/detail-page-canvas/konva-fallback.ts +10 -4
- package/package.json +4 -1
- package/styles/canvas-bridge.css +35 -0
- package/styles/tokens.css +103 -0
|
@@ -87,14 +87,14 @@ export function DetailPageFontPicker({
|
|
|
87
87
|
/>
|
|
88
88
|
</span>
|
|
89
89
|
{font.latinOnly ? (
|
|
90
|
-
<span className="shrink-0 rounded border border-
|
|
90
|
+
<span className="shrink-0 rounded border border-dpe-ink-200 px-1 py-0.5 text-[10px] leading-none text-dpe-ink-500">
|
|
91
91
|
{t("detailPage.properties.fontLatinOnly")}
|
|
92
92
|
</span>
|
|
93
93
|
) : null}
|
|
94
94
|
{loading === font.family ? (
|
|
95
95
|
<LoaderCircle
|
|
96
96
|
aria-label={t("detailPage.properties.fontLoading")}
|
|
97
|
-
className="shrink-0 animate-spin text-
|
|
97
|
+
className="shrink-0 animate-spin text-dpe-ink-500"
|
|
98
98
|
size={15}
|
|
99
99
|
/>
|
|
100
100
|
) : value === font.family ? (
|
|
@@ -116,10 +116,10 @@ export function DetailPageFontPicker({
|
|
|
116
116
|
type="button"
|
|
117
117
|
aria-label={t("detailPage.properties.chooseFont")}
|
|
118
118
|
aria-expanded={open}
|
|
119
|
-
className="flex min-w-0 items-center justify-between gap-2 rounded-md border border-
|
|
119
|
+
className="flex min-w-0 items-center justify-between gap-2 rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface px-2 py-1.5 text-left text-sm text-dpe-ink-900 outline-none hover:bg-dpe-ink-50 focus:border-dpe-ink-400"
|
|
120
120
|
>
|
|
121
121
|
<span className="min-w-0 truncate">{selected?.label ?? value}</span>
|
|
122
|
-
<ChevronDown aria-hidden="true" className="shrink-0 text-
|
|
122
|
+
<ChevronDown aria-hidden="true" className="shrink-0 text-dpe-ink-400" size={14} />
|
|
123
123
|
</button>
|
|
124
124
|
</PopoverPrimitive.Trigger>
|
|
125
125
|
<PopoverPrimitive.Portal>
|
|
@@ -128,12 +128,12 @@ export function DetailPageFontPicker({
|
|
|
128
128
|
align="start"
|
|
129
129
|
sideOffset={8}
|
|
130
130
|
collisionPadding={12}
|
|
131
|
-
className="z-[120] w-[340px] overflow-hidden rounded-lg border border-
|
|
131
|
+
className="z-[120] w-[340px] overflow-hidden rounded-dpe-lg border border-dpe-ink-200 bg-dpe-surface shadow-[0_10px_32px_rgba(0,0,0,0.14)]"
|
|
132
132
|
>
|
|
133
133
|
<div
|
|
134
134
|
role="group"
|
|
135
135
|
aria-label={t("detailPage.properties.fontTagFilter")}
|
|
136
|
-
className="flex flex-wrap gap-1 border-b border-
|
|
136
|
+
className="flex flex-wrap gap-1 border-b border-dpe-ink-100 px-2 py-2"
|
|
137
137
|
>
|
|
138
138
|
<button
|
|
139
139
|
type="button"
|
|
@@ -141,8 +141,8 @@ export function DetailPageFontPicker({
|
|
|
141
141
|
onClick={() => setTags([])}
|
|
142
142
|
className={`rounded-full border px-2 py-0.5 text-[11px] leading-4 transition-colors ${
|
|
143
143
|
tags.length === 0
|
|
144
|
-
? "border-
|
|
145
|
-
: "border-
|
|
144
|
+
? "border-dpe-ink-900 bg-dpe-ink-900 text-dpe-on-accent"
|
|
145
|
+
: "border-dpe-ink-200 text-dpe-ink-600 hover:bg-dpe-ink-50"
|
|
146
146
|
}`}
|
|
147
147
|
>
|
|
148
148
|
{t("detailPage.properties.fontTagAll")}
|
|
@@ -155,8 +155,8 @@ export function DetailPageFontPicker({
|
|
|
155
155
|
onClick={() => toggleTag(tag)}
|
|
156
156
|
className={`rounded-full border px-2 py-0.5 text-[11px] leading-4 transition-colors ${
|
|
157
157
|
tags.includes(tag)
|
|
158
|
-
? "border-
|
|
159
|
-
: "border-
|
|
158
|
+
? "border-dpe-ink-900 bg-dpe-ink-900 text-dpe-on-accent"
|
|
159
|
+
: "border-dpe-ink-200 text-dpe-ink-600 hover:bg-dpe-ink-50"
|
|
160
160
|
}`}
|
|
161
161
|
>
|
|
162
162
|
{t(`detailPage.fontTags.${tag}`)}
|
|
@@ -202,17 +202,17 @@ export function DetailPageFontPicker({
|
|
|
202
202
|
</CommandList>
|
|
203
203
|
</Command>
|
|
204
204
|
{error ? (
|
|
205
|
-
<p role="alert" className="border-t border-
|
|
205
|
+
<p role="alert" className="border-t border-dpe-danger-100 bg-dpe-danger-50 px-3 py-2 text-xs text-dpe-danger-600">
|
|
206
206
|
{t("detailPage.properties.fontLoadFailed")}
|
|
207
207
|
</p>
|
|
208
208
|
) : null}
|
|
209
209
|
{selected ? (
|
|
210
|
-
<div className="border-t border-
|
|
210
|
+
<div className="border-t border-dpe-ink-100 px-3 py-2 text-[11px] leading-4 text-dpe-ink-500">
|
|
211
211
|
<a
|
|
212
212
|
href={selected.licenseUrl}
|
|
213
213
|
target="_blank"
|
|
214
214
|
rel="noreferrer"
|
|
215
|
-
className="font-medium text-
|
|
215
|
+
className="font-dpe-medium text-dpe-ink-700 underline decoration-dpe-ink-300 underline-offset-2"
|
|
216
216
|
>
|
|
217
217
|
{t("detailPage.properties.fontLicense")}: {selected.licenseName}
|
|
218
218
|
</a>
|
|
@@ -223,7 +223,7 @@ export function DetailPageFontPicker({
|
|
|
223
223
|
) : null}
|
|
224
224
|
</div>
|
|
225
225
|
) : null}
|
|
226
|
-
<PopoverPrimitive.Arrow className="fill-
|
|
226
|
+
<PopoverPrimitive.Arrow className="fill-dpe-on-accent" />
|
|
227
227
|
</PopoverPrimitive.Content>
|
|
228
228
|
</PopoverPrimitive.Portal>
|
|
229
229
|
</PopoverPrimitive.Root>
|
|
@@ -46,10 +46,10 @@ function HistoryButton({
|
|
|
46
46
|
disabled={disabled}
|
|
47
47
|
onClick={onClick}
|
|
48
48
|
className={[
|
|
49
|
-
"flex h-9 w-9 items-center justify-center rounded-md transition-colors",
|
|
49
|
+
"flex h-9 w-9 items-center justify-center rounded-dpe-md transition-colors",
|
|
50
50
|
disabled
|
|
51
|
-
? "cursor-not-allowed text-
|
|
52
|
-
: "text-
|
|
51
|
+
? "cursor-not-allowed text-dpe-ink-300"
|
|
52
|
+
: "text-dpe-ink-600 hover:bg-dpe-ink-100 hover:text-dpe-ink-900",
|
|
53
53
|
].join(" ")}
|
|
54
54
|
>
|
|
55
55
|
{children}
|
|
@@ -429,6 +429,69 @@ export interface DetailPageHostSlots {
|
|
|
429
429
|
* 도메인을 알아야 묶이므로 앱이 꽂는다. 안 꽂으면 탭 없이 브랜드 자산만 뜬다.
|
|
430
430
|
*/
|
|
431
431
|
AuthoredImagesPanel?: ComponentType<{ store: unknown }>;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 상단 헤더를 통째로 갈아 끼운다.
|
|
435
|
+
*
|
|
436
|
+
* 색·모서리는 토큰(`tokens.css`)으로 바꾸면 되지만, "무엇이 어디에 놓이는가" 는
|
|
437
|
+
* 토큰이 못 바꾼다. 앱마다 헤더에 걸고 싶은 것이 다르므로 여기서 연다.
|
|
438
|
+
*
|
|
439
|
+
* 어려운 조각(되돌리기·내보내기 대화상자)은 만들어서 `parts` 로 넘긴다 — 그것까지
|
|
440
|
+
* 다시 만들라고 하면 슬롯이 아니라 포크가 된다.
|
|
441
|
+
*/
|
|
442
|
+
EditorHeader?: ComponentType<EditorHeaderSlotProps>;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 우측 인스펙터를 감싸거나 갈아 끼운다. 기본 인스펙터는 `defaultInspector` 로 온다 —
|
|
446
|
+
* 자기 크롬만 두르고 속은 그대로 쓰는 것이 보통이다.
|
|
447
|
+
*/
|
|
448
|
+
EditorInspector?: ComponentType<EditorInspectorSlotProps>;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 좌측 레일+패널 껍데기를 갈아 끼운다. 섹션 목록(탭·패널 컴포넌트)은 그대로 넘어가므로
|
|
452
|
+
* 앱은 배치만 다시 짜면 된다.
|
|
453
|
+
*/
|
|
454
|
+
EditorSidebar?: ComponentType<EditorSidebarSlotProps>;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** 헤더 슬롯이 받는 것. */
|
|
458
|
+
export interface EditorHeaderSlotProps {
|
|
459
|
+
/** 표시용 이름. 비어 있으면 편집기가 기본 문구를 채워서 준다. */
|
|
460
|
+
productName: string;
|
|
461
|
+
onBack?: () => void;
|
|
462
|
+
save: {
|
|
463
|
+
run: () => void;
|
|
464
|
+
saving: boolean;
|
|
465
|
+
/** 방금 저장됐다. 잠시 뒤 스스로 꺼진다. */
|
|
466
|
+
ok: boolean;
|
|
467
|
+
error: string | null;
|
|
468
|
+
};
|
|
469
|
+
/** 편집기가 만들어 주는 조각들. 자리만 정하면 된다. */
|
|
470
|
+
parts: {
|
|
471
|
+
history: ReactNode;
|
|
472
|
+
download: ReactNode;
|
|
473
|
+
/** 앱이 `headerActions` 로 넘긴 것(크레딧·알림·언어). */
|
|
474
|
+
actions: ReactNode;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** 인스펙터 슬롯이 받는 것. */
|
|
479
|
+
export interface EditorInspectorSlotProps {
|
|
480
|
+
store: unknown;
|
|
481
|
+
defaultInspector: ReactNode;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** 좌측 껍데기 슬롯이 받는 것. */
|
|
485
|
+
export interface EditorSidebarSlotProps {
|
|
486
|
+
store: unknown;
|
|
487
|
+
sections: ReadonlyArray<{
|
|
488
|
+
name: string;
|
|
489
|
+
Tab: ComponentType<Record<string, unknown>>;
|
|
490
|
+
Panel: ComponentType<{ store: unknown }>;
|
|
491
|
+
visibleInList?: boolean;
|
|
492
|
+
}>;
|
|
493
|
+
/** 처음 열어 둘 섹션 이름. */
|
|
494
|
+
defaultSection: string;
|
|
432
495
|
}
|
|
433
496
|
|
|
434
497
|
export interface DetailPageHost {
|
|
@@ -142,20 +142,20 @@ export function DetailPageIconsPanel({ store }: { store: unknown }) {
|
|
|
142
142
|
|
|
143
143
|
{group === "logos" ? (
|
|
144
144
|
// 파일은 CC0지만 로고 자체는 상표다. 안내를 지우면 이용 조건을 어기는 쪽으로 샌다.
|
|
145
|
-
<p className="mx-3 mt-2 shrink-0 rounded-md bg-
|
|
145
|
+
<p className="mx-3 mt-2 shrink-0 rounded-dpe-md bg-dpe-warn-50 px-2 py-1.5 text-[11px] leading-relaxed text-dpe-warn-800">
|
|
146
146
|
{t("detailPage.icons.logoTrademark")}
|
|
147
147
|
</p>
|
|
148
148
|
) : null}
|
|
149
149
|
|
|
150
150
|
<div ref={scrollRef} className="mt-3 min-h-0 flex-1 overflow-y-auto px-3">
|
|
151
151
|
{icons.isPending ? (
|
|
152
|
-
<div className="flex justify-center py-6 text-
|
|
152
|
+
<div className="flex justify-center py-6 text-dpe-ink-400">
|
|
153
153
|
<Loader2 aria-hidden="true" className="animate-spin" size={20} />
|
|
154
154
|
</div>
|
|
155
155
|
) : icons.isError ? (
|
|
156
|
-
<p className="text-xs text-
|
|
156
|
+
<p className="text-xs text-dpe-danger-600">{t("detailPage.icons.failed")}</p>
|
|
157
157
|
) : cells.length === 0 ? (
|
|
158
|
-
<div className="flex flex-col items-center gap-2 py-6 text-center text-
|
|
158
|
+
<div className="flex flex-col items-center gap-2 py-6 text-center text-dpe-ink-400">
|
|
159
159
|
<ImageOff aria-hidden="true" size={20} />
|
|
160
160
|
<p className="text-xs">{t("detailPage.icons.empty")}</p>
|
|
161
161
|
</div>
|
|
@@ -169,7 +169,7 @@ export function DetailPageIconsPanel({ store }: { store: unknown }) {
|
|
|
169
169
|
onClick={() => insert(item, markup)}
|
|
170
170
|
title={t("detailPage.shapes.insertHint")}
|
|
171
171
|
aria-label={item.id}
|
|
172
|
-
className="flex aspect-square items-center justify-center rounded-lg border border-
|
|
172
|
+
className="flex aspect-square items-center justify-center rounded-dpe-lg border border-dpe-ink-200 p-1.5 hover:border-dpe-ink-400"
|
|
173
173
|
>
|
|
174
174
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
175
175
|
<img
|
|
@@ -182,7 +182,7 @@ export function DetailPageIconsPanel({ store }: { store: unknown }) {
|
|
|
182
182
|
</div>
|
|
183
183
|
<div ref={sentinelRef} aria-hidden="true" className="h-px" />
|
|
184
184
|
{isFetchingNextPage ? (
|
|
185
|
-
<div className="flex justify-center py-3 text-
|
|
185
|
+
<div className="flex justify-center py-3 text-dpe-ink-400">
|
|
186
186
|
<Loader2 aria-hidden="true" className="animate-spin" size={16} />
|
|
187
187
|
</div>
|
|
188
188
|
) : hasNextPage ? (
|
|
@@ -190,13 +190,13 @@ export function DetailPageIconsPanel({ store }: { store: unknown }) {
|
|
|
190
190
|
<button
|
|
191
191
|
type="button"
|
|
192
192
|
onClick={() => void fetchNextPage()}
|
|
193
|
-
className="mt-2 w-full rounded-md border border-
|
|
193
|
+
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:bg-dpe-ink-50"
|
|
194
194
|
>
|
|
195
195
|
{t("detailPage.icons.loadMore")}
|
|
196
196
|
</button>
|
|
197
197
|
) : truncated ? (
|
|
198
198
|
// 조용히 자르면 "이게 전부"로 읽힌다.
|
|
199
|
-
<p className="mt-2 px-1 text-[11px] text-
|
|
199
|
+
<p className="mt-2 px-1 text-[11px] text-dpe-ink-400">
|
|
200
200
|
{t("detailPage.icons.truncated")}
|
|
201
201
|
</p>
|
|
202
202
|
) : null}
|
|
@@ -204,7 +204,7 @@ export function DetailPageIconsPanel({ store }: { store: unknown }) {
|
|
|
204
204
|
)}
|
|
205
205
|
</div>
|
|
206
206
|
|
|
207
|
-
<p className="shrink-0 border-t border-
|
|
207
|
+
<p className="shrink-0 border-t border-dpe-ink-100 px-3 py-2 text-[11px] leading-relaxed text-dpe-ink-500">
|
|
208
208
|
{t("detailPage.icons.licenseHint")}
|
|
209
209
|
</p>
|
|
210
210
|
</div>
|
|
@@ -226,7 +226,7 @@ function SegmentedGroup<T extends string>({
|
|
|
226
226
|
<div
|
|
227
227
|
role="group"
|
|
228
228
|
aria-label={label}
|
|
229
|
-
className="flex flex-1 rounded-md border border-
|
|
229
|
+
className="flex flex-1 rounded-dpe-md border border-dpe-ink-200 p-0.5"
|
|
230
230
|
>
|
|
231
231
|
{options.map((option) => {
|
|
232
232
|
const active = option.value === value;
|
|
@@ -236,10 +236,10 @@ function SegmentedGroup<T extends string>({
|
|
|
236
236
|
type="button"
|
|
237
237
|
aria-pressed={active}
|
|
238
238
|
onClick={() => onChange(option.value)}
|
|
239
|
-
className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition-colors ${
|
|
239
|
+
className={`flex-1 rounded px-2 py-1 text-[11px] font-dpe-medium transition-colors ${
|
|
240
240
|
active
|
|
241
|
-
? "bg-
|
|
242
|
-
: "text-
|
|
241
|
+
? "bg-dpe-ink-900 text-dpe-on-accent"
|
|
242
|
+
: "text-dpe-ink-500 hover:bg-dpe-ink-100"
|
|
243
243
|
}`}
|
|
244
244
|
>
|
|
245
245
|
{option.label}
|
|
@@ -415,30 +415,30 @@ export function DetailPageKonvaJsonPreview({
|
|
|
415
415
|
const scale = 0.48;
|
|
416
416
|
|
|
417
417
|
return (
|
|
418
|
-
<div className="h-screen overflow-auto bg-
|
|
419
|
-
<div className="sticky top-0 z-10 flex h-12 items-center justify-between border-b border-
|
|
418
|
+
<div className="h-screen overflow-auto bg-dpe-ink-100">
|
|
419
|
+
<div className="sticky top-0 z-10 flex h-12 items-center justify-between border-b border-dpe-ink-200 bg-dpe-surface px-4">
|
|
420
420
|
<div>
|
|
421
|
-
<p className="text-sm font-semibold text-
|
|
422
|
-
<p className="text-xs text-
|
|
421
|
+
<p className="text-sm font-dpe-semibold text-dpe-ink-950">Native Konva JSON Preview</p>
|
|
422
|
+
<p className="text-xs text-dpe-ink-500">
|
|
423
423
|
Canvas 없이 JSON을 직접 렌더링하는 최소 fallback
|
|
424
424
|
</p>
|
|
425
425
|
</div>
|
|
426
426
|
<button
|
|
427
427
|
type="button"
|
|
428
428
|
onClick={() => setShowSlots((value) => !value)}
|
|
429
|
-
className="inline-flex h-8 items-center rounded-md border border-
|
|
429
|
+
className="inline-flex h-8 items-center rounded-dpe-md border border-dpe-ink-200 px-3 text-xs font-dpe-semibold text-dpe-ink-700"
|
|
430
430
|
>
|
|
431
431
|
{showSlots ? "Hide slots" : "Show slots"}
|
|
432
432
|
</button>
|
|
433
433
|
</div>
|
|
434
434
|
<div className="mx-auto flex w-full max-w-[480px] flex-col gap-6 px-4 py-6">
|
|
435
435
|
{pages.map((page, pageIndex) => (
|
|
436
|
-
<section key={page.id} className="overflow-hidden rounded-md border border-
|
|
437
|
-
<div className="flex items-center justify-between border-b border-
|
|
438
|
-
<span className="text-xs font-semibold text-
|
|
436
|
+
<section key={page.id} className="overflow-hidden rounded-dpe-md border border-dpe-ink-200 bg-dpe-surface shadow-sm">
|
|
437
|
+
<div className="flex items-center justify-between border-b border-dpe-ink-200 px-3 py-2">
|
|
438
|
+
<span className="text-xs font-dpe-semibold text-dpe-ink-800">
|
|
439
439
|
{page.id || `page-${pageIndex + 1}`}
|
|
440
440
|
</span>
|
|
441
|
-
<span className="text-xs text-
|
|
441
|
+
<span className="text-xs text-dpe-ink-500">
|
|
442
442
|
{Math.round(page.width)} x {Math.round(page.height)}
|
|
443
443
|
</span>
|
|
444
444
|
</div>
|
|
@@ -167,10 +167,10 @@ function IconButton({
|
|
|
167
167
|
}}
|
|
168
168
|
className={[
|
|
169
169
|
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
|
170
|
-
active ? "text-
|
|
170
|
+
active ? "text-dpe-ink-800" : "text-dpe-ink-400",
|
|
171
171
|
disabled
|
|
172
172
|
? "cursor-not-allowed opacity-30"
|
|
173
|
-
: "hover:bg-
|
|
173
|
+
: "hover:bg-dpe-ink-200/70 hover:text-dpe-ink-800",
|
|
174
174
|
].join(" ")}
|
|
175
175
|
>
|
|
176
176
|
{children}
|
|
@@ -394,15 +394,15 @@ const LayerRow = observer(function LayerRow({
|
|
|
394
394
|
}
|
|
395
395
|
}}
|
|
396
396
|
className={[
|
|
397
|
-
"group relative flex items-center gap-1 rounded-md pr-1.5 text-
|
|
397
|
+
"group relative flex items-center gap-1 rounded-dpe-md pr-1.5 text-dpe-ink-700",
|
|
398
398
|
"min-h-[30px] cursor-default select-none",
|
|
399
399
|
dragging ? "opacity-40" : "",
|
|
400
|
-
over === "inside" ? "ring-2 ring-inset ring-
|
|
400
|
+
over === "inside" ? "ring-2 ring-inset ring-dpe-select-400" : "",
|
|
401
401
|
selected
|
|
402
|
-
? "bg-
|
|
402
|
+
? "bg-dpe-select-200 text-dpe-select-950"
|
|
403
403
|
: context.has(el.id)
|
|
404
|
-
? "bg-
|
|
405
|
-
: "hover:bg-
|
|
404
|
+
? "bg-dpe-select-50 text-dpe-select-900 hover:bg-dpe-select-100"
|
|
405
|
+
: "hover:bg-dpe-ink-100",
|
|
406
406
|
].join(" ")}
|
|
407
407
|
style={{ paddingLeft: 6 + depth * 14 }}
|
|
408
408
|
>
|
|
@@ -411,7 +411,7 @@ const LayerRow = observer(function LayerRow({
|
|
|
411
411
|
aria-hidden="true"
|
|
412
412
|
data-testid={`drop-line-${over}`}
|
|
413
413
|
className={[
|
|
414
|
-
"pointer-events-none absolute inset-x-1 h-0.5 rounded-full bg-
|
|
414
|
+
"pointer-events-none absolute inset-x-1 h-0.5 rounded-full bg-dpe-select-500",
|
|
415
415
|
over === "before" ? "top-0" : "bottom-0",
|
|
416
416
|
].join(" ")}
|
|
417
417
|
/>
|
|
@@ -426,7 +426,7 @@ const LayerRow = observer(function LayerRow({
|
|
|
426
426
|
e.stopPropagation();
|
|
427
427
|
onToggle(el.id);
|
|
428
428
|
}}
|
|
429
|
-
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-
|
|
429
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-dpe-ink-400 hover:bg-dpe-ink-200/70 hover:text-dpe-ink-700"
|
|
430
430
|
>
|
|
431
431
|
{isCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
|
432
432
|
</button>
|
|
@@ -436,13 +436,13 @@ const LayerRow = observer(function LayerRow({
|
|
|
436
436
|
|
|
437
437
|
<Icon
|
|
438
438
|
size={14}
|
|
439
|
-
className={["shrink-0", selected ? "text-
|
|
439
|
+
className={["shrink-0", selected ? "text-dpe-select-500" : "text-dpe-ink-400"].join(" ")}
|
|
440
440
|
/>
|
|
441
441
|
|
|
442
442
|
<span
|
|
443
443
|
className={[
|
|
444
444
|
"flex-1 truncate text-[13px]",
|
|
445
|
-
hidden ? "text-
|
|
445
|
+
hidden ? "text-dpe-ink-300 line-through" : "",
|
|
446
446
|
].join(" ")}
|
|
447
447
|
title={label}
|
|
448
448
|
>
|
|
@@ -583,17 +583,17 @@ export const DetailPageLayersPanel = observer(function DetailPageLayersPanel({
|
|
|
583
583
|
|
|
584
584
|
return (
|
|
585
585
|
<div className="flex h-full min-h-0 flex-col">
|
|
586
|
-
<div className="flex items-center justify-between px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-
|
|
586
|
+
<div className="flex items-center justify-between px-3 py-2.5 text-[11px] font-dpe-semibold uppercase tracking-[0.06em] text-dpe-ink-400">
|
|
587
587
|
<span>{t("detailPage.layers.title")}</span>
|
|
588
588
|
{/* 머리글 숫자는 **맨 위 층의 줄 수**다. 그룹을 펴면 줄은 늘지만 레이어 수는
|
|
589
589
|
그대로다 — 펼침 상태에 따라 숫자가 오르내리면 그건 개수가 아니다. */}
|
|
590
|
-
<span className="tabular-nums text-
|
|
590
|
+
<span className="tabular-nums text-dpe-ink-300">
|
|
591
591
|
{rows.filter((row) => row.depth === 0).length}
|
|
592
592
|
</span>
|
|
593
593
|
</div>
|
|
594
594
|
<div className="min-h-0 flex-1 overflow-y-auto px-1.5 pb-4">
|
|
595
595
|
{rows.length === 0 ? (
|
|
596
|
-
<p className="px-2 py-6 text-center text-xs text-
|
|
596
|
+
<p className="px-2 py-6 text-center text-xs text-dpe-ink-400">
|
|
597
597
|
{t("detailPage.layers.empty")}
|
|
598
598
|
</p>
|
|
599
599
|
) : (
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
BrandAssetKind,
|
|
14
14
|
} from "./detail-page-host-context";
|
|
15
15
|
import { insertPersonalImage } from "../../lib/detail-page/insert-image";
|
|
16
|
+
import { useProgressiveReveal } from "../../lib/detail-page/use-progressive-reveal";
|
|
16
17
|
|
|
17
18
|
const UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp,image/svg+xml";
|
|
18
19
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -52,7 +53,7 @@ export function DetailPageMyImagesPanel({ store }: { store: unknown }) {
|
|
|
52
53
|
|
|
53
54
|
return (
|
|
54
55
|
<div className="flex h-full flex-col">
|
|
55
|
-
<div className="flex gap-1 border-b border-
|
|
56
|
+
<div className="flex gap-1 border-b border-dpe-ink-200 px-4 pt-3">
|
|
56
57
|
<SourceTab
|
|
57
58
|
label={t("detailPage.brandAssets.sourceBrand")}
|
|
58
59
|
active={source === "brand"}
|
|
@@ -90,10 +91,10 @@ function SourceTab({
|
|
|
90
91
|
onClick={onClick}
|
|
91
92
|
aria-pressed={active}
|
|
92
93
|
className={
|
|
93
|
-
"-mb-px border-b-2 px-2.5 pb-2 text-[11px] font-medium " +
|
|
94
|
+
"-mb-px border-b-2 px-2.5 pb-2 text-[11px] font-dpe-medium " +
|
|
94
95
|
(active
|
|
95
|
-
? "border-
|
|
96
|
-
: "border-transparent text-
|
|
96
|
+
? "border-dpe-ink-800 text-dpe-ink-800"
|
|
97
|
+
: "border-transparent text-dpe-ink-400 hover:text-dpe-ink-600")
|
|
97
98
|
}
|
|
98
99
|
>
|
|
99
100
|
{label}
|
|
@@ -120,6 +121,8 @@ function BrandAssetGallery({ store }: { store: unknown }) {
|
|
|
120
121
|
staleTime: 4 * 60_000,
|
|
121
122
|
});
|
|
122
123
|
const items = (assetsQuery.data ?? []).filter(isImageAsset);
|
|
124
|
+
// 브랜드를 옮기면 다시 처음부터 센다.
|
|
125
|
+
const reveal = useProgressiveReveal(items.length, { resetKey: activeBrandId });
|
|
123
126
|
|
|
124
127
|
const uploadMutation = useMutation({
|
|
125
128
|
mutationFn: async (file: File) => {
|
|
@@ -163,7 +166,7 @@ function BrandAssetGallery({ store }: { store: unknown }) {
|
|
|
163
166
|
<div className="flex h-full flex-col p-4">
|
|
164
167
|
<BrandPanelHeader onRefresh={() => void assetsQuery.refetch()} />
|
|
165
168
|
|
|
166
|
-
<label className="mb-3 flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-md border border-dashed border-
|
|
169
|
+
<label className="mb-3 flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-dpe-md border border-dashed border-dpe-ink-300 text-dpe-ink-400 hover:border-dpe-ink-400 hover:text-dpe-ink-600">
|
|
167
170
|
{uploadMutation.isPending ? (
|
|
168
171
|
<Loader2 aria-hidden="true" className="animate-spin" size={18} />
|
|
169
172
|
) : (
|
|
@@ -182,24 +185,24 @@ function BrandAssetGallery({ store }: { store: unknown }) {
|
|
|
182
185
|
</label>
|
|
183
186
|
|
|
184
187
|
{brandsLoading || assetsQuery.isLoading ? (
|
|
185
|
-
<div className="flex flex-1 items-center justify-center text-
|
|
188
|
+
<div className="flex flex-1 items-center justify-center text-dpe-ink-400">
|
|
186
189
|
<Loader2 aria-hidden="true" className="animate-spin" size={22} />
|
|
187
190
|
</div>
|
|
188
191
|
) : error || assetsQuery.error ? (
|
|
189
|
-
<p className="text-xs font-medium text-
|
|
192
|
+
<p className="text-xs font-dpe-medium text-dpe-danger-600">
|
|
190
193
|
{error ?? t("detailPage.brandAssets.loadFailed")}
|
|
191
194
|
</p>
|
|
192
195
|
) : !activeBrand ? (
|
|
193
|
-
<p className="text-xs text-
|
|
196
|
+
<p className="text-xs text-dpe-ink-400">
|
|
194
197
|
{t("detailPage.brandAssets.noBrand")}
|
|
195
198
|
</p>
|
|
196
199
|
) : items.length === 0 ? (
|
|
197
|
-
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-
|
|
200
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-dpe-ink-400">
|
|
198
201
|
<ImageOff aria-hidden="true" size={22} />
|
|
199
202
|
<p className="text-xs">{t("detailPage.brandAssets.imagesEmpty")}</p>
|
|
200
203
|
<Link
|
|
201
204
|
href="/branding/moodboard"
|
|
202
|
-
className="inline-flex items-center gap-1 text-[11px] font-medium text-
|
|
205
|
+
className="inline-flex items-center gap-1 text-[11px] font-dpe-medium text-dpe-ink-600 underline underline-offset-2"
|
|
203
206
|
>
|
|
204
207
|
{t("detailPage.brandAssets.openMoodboard")}
|
|
205
208
|
<ExternalLink size={11} />
|
|
@@ -207,10 +210,11 @@ function BrandAssetGallery({ store }: { store: unknown }) {
|
|
|
207
210
|
</div>
|
|
208
211
|
) : (
|
|
209
212
|
<div className="grid grid-cols-2 gap-2 overflow-y-auto">
|
|
210
|
-
{items.map((asset) => (
|
|
213
|
+
{items.slice(0, reveal.visible).map((asset) => (
|
|
211
214
|
<div
|
|
212
215
|
key={asset.id}
|
|
213
|
-
|
|
216
|
+
data-dpe-part="asset-card"
|
|
217
|
+
className="group relative overflow-hidden rounded-dpe-md border border-dpe-ink-200 hover:border-dpe-ink-500"
|
|
214
218
|
>
|
|
215
219
|
<button
|
|
216
220
|
type="button"
|
|
@@ -228,23 +232,34 @@ function BrandAssetGallery({ store }: { store: unknown }) {
|
|
|
228
232
|
<img
|
|
229
233
|
src={asset.download_url ?? ""}
|
|
230
234
|
crossOrigin="anonymous"
|
|
235
|
+
loading="lazy"
|
|
236
|
+
decoding="async"
|
|
231
237
|
alt={asset.display_name ?? asset.filename}
|
|
232
238
|
className="h-24 w-full object-cover transition group-hover:opacity-90"
|
|
233
239
|
/>
|
|
234
240
|
</button>
|
|
235
|
-
<span className="pointer-events-none absolute bottom-1.5 left-1.5 max-w-[calc(100%-12px)] truncate rounded-sm bg-
|
|
241
|
+
<span className="pointer-events-none absolute bottom-1.5 left-1.5 max-w-[calc(100%-12px)] truncate rounded-dpe-sm bg-dpe-scrim/70 px-1.5 py-0.5 text-[10px] text-dpe-on-accent">
|
|
236
242
|
{asset.display_name ?? asset.filename}
|
|
237
243
|
</span>
|
|
238
244
|
<button
|
|
239
245
|
type="button"
|
|
240
246
|
onClick={() => deleteMutation.mutate(asset)}
|
|
241
|
-
className="absolute right-1 top-1 hidden h-6 w-6 items-center justify-center rounded-md bg-
|
|
247
|
+
className="absolute right-1 top-1 hidden h-6 w-6 items-center justify-center rounded-dpe-md bg-dpe-surface/95 text-dpe-ink-400 shadow-sm hover:text-dpe-danger-600 group-hover:flex"
|
|
242
248
|
aria-label={t("detailPage.brandAssets.delete")}
|
|
243
249
|
>
|
|
244
250
|
<Trash2 aria-hidden="true" size={13} />
|
|
245
251
|
</button>
|
|
246
252
|
</div>
|
|
247
253
|
))}
|
|
254
|
+
{reveal.hasMore && (
|
|
255
|
+
<div
|
|
256
|
+
ref={reveal.sentinelRef}
|
|
257
|
+
data-testid="brand-assets-sentinel"
|
|
258
|
+
className="col-span-2 flex h-10 items-center justify-center text-dpe-ink-300"
|
|
259
|
+
>
|
|
260
|
+
<Loader2 aria-hidden="true" className="animate-spin" size={16} />
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
248
263
|
</div>
|
|
249
264
|
)}
|
|
250
265
|
</div>
|
|
@@ -84,7 +84,7 @@ export function DetailPageMyShapesPanel({ store }: { store: unknown }) {
|
|
|
84
84
|
<div className="flex h-full flex-col p-4">
|
|
85
85
|
<BrandPanelHeader onRefresh={() => void assetsQuery.refetch()} />
|
|
86
86
|
|
|
87
|
-
<label className="mb-3 flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-md border border-dashed border-
|
|
87
|
+
<label className="mb-3 flex h-16 cursor-pointer flex-col items-center justify-center gap-1 rounded-dpe-md border border-dashed border-dpe-ink-300 text-dpe-ink-400 hover:border-dpe-ink-400 hover:text-dpe-ink-600">
|
|
88
88
|
{uploadMutation.isPending ? (
|
|
89
89
|
<Loader2 aria-hidden="true" className="animate-spin" size={18} />
|
|
90
90
|
) : (
|
|
@@ -115,19 +115,19 @@ export function DetailPageMyShapesPanel({ store }: { store: unknown }) {
|
|
|
115
115
|
</label>
|
|
116
116
|
|
|
117
117
|
{brandsLoading || assetsQuery.isLoading ? (
|
|
118
|
-
<div className="flex flex-1 items-center justify-center text-
|
|
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
|
) : assetsQuery.error ? (
|
|
122
|
-
<p className="text-xs font-medium text-
|
|
122
|
+
<p className="text-xs font-dpe-medium text-dpe-danger-600">
|
|
123
123
|
{t("detailPage.brandAssets.loadFailed")}
|
|
124
124
|
</p>
|
|
125
125
|
) : !activeBrand ? (
|
|
126
|
-
<p className="text-xs text-
|
|
126
|
+
<p className="text-xs text-dpe-ink-400">
|
|
127
127
|
{t("detailPage.brandAssets.noBrand")}
|
|
128
128
|
</p>
|
|
129
129
|
) : items.length === 0 ? (
|
|
130
|
-
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-
|
|
130
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-dpe-ink-400">
|
|
131
131
|
<Shapes aria-hidden="true" size={22} />
|
|
132
132
|
<p className="text-xs">{t("detailPage.brandAssets.shapesEmpty")}</p>
|
|
133
133
|
</div>
|
|
@@ -136,7 +136,7 @@ export function DetailPageMyShapesPanel({ store }: { store: unknown }) {
|
|
|
136
136
|
{items.map((asset) => (
|
|
137
137
|
<div
|
|
138
138
|
key={asset.id}
|
|
139
|
-
className="group relative aspect-square rounded-md border border-
|
|
139
|
+
className="group relative aspect-square rounded-dpe-md border border-dpe-ink-200 hover:border-dpe-ink-500"
|
|
140
140
|
>
|
|
141
141
|
<button
|
|
142
142
|
type="button"
|
|
@@ -146,7 +146,7 @@ export function DetailPageMyShapesPanel({ store }: { store: unknown }) {
|
|
|
146
146
|
title={t("detailPage.brandAssets.insertHint")}
|
|
147
147
|
>
|
|
148
148
|
{insertingId === asset.id ? (
|
|
149
|
-
<Loader2 className="animate-spin text-
|
|
149
|
+
<Loader2 className="animate-spin text-dpe-ink-400" size={18} />
|
|
150
150
|
) : (
|
|
151
151
|
// eslint-disable-next-line @next/next/no-img-element
|
|
152
152
|
<img
|
|
@@ -160,7 +160,7 @@ export function DetailPageMyShapesPanel({ store }: { store: unknown }) {
|
|
|
160
160
|
<button
|
|
161
161
|
type="button"
|
|
162
162
|
onClick={() => deleteMutation.mutate(asset)}
|
|
163
|
-
className="absolute right-1 top-1 hidden h-6 w-6 items-center justify-center rounded-md bg-
|
|
163
|
+
className="absolute right-1 top-1 hidden h-6 w-6 items-center justify-center rounded-dpe-md bg-dpe-surface/95 text-dpe-ink-400 shadow-sm hover:text-dpe-danger-600 group-hover:flex"
|
|
164
164
|
aria-label={t("detailPage.brandAssets.delete")}
|
|
165
165
|
>
|
|
166
166
|
<Trash2 aria-hidden="true" size={13} />
|