@leviosa-ai/detail-page-editor 0.1.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.
- package/components/cardnews/color-input.tsx +380 -0
- package/components/detail-page/ai-generate-panel.tsx +763 -0
- package/components/detail-page/annotation-canvas.tsx +860 -0
- package/components/detail-page/annotation-dialog.tsx +397 -0
- package/components/detail-page/bubble-tail-overlay.tsx +209 -0
- package/components/detail-page/canvas-context-menu.tsx +234 -0
- package/components/detail-page/canvas-observer.tsx +68 -0
- package/components/detail-page/canvas-overlay-host.tsx +44 -0
- package/components/detail-page/canvas-store-facade.ts +51 -0
- package/components/detail-page/chart-inspector.tsx +531 -0
- package/components/detail-page/design-reference-picker.tsx +383 -0
- package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
- package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
- package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
- package/components/detail-page/detail-page-charts-panel.tsx +104 -0
- package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
- package/components/detail-page/detail-page-download-dialog.tsx +524 -0
- package/components/detail-page/detail-page-editor.tsx +974 -0
- package/components/detail-page/detail-page-elements-panel.tsx +117 -0
- package/components/detail-page/detail-page-font-picker.tsx +231 -0
- package/components/detail-page/detail-page-history-buttons.tsx +87 -0
- package/components/detail-page/detail-page-host-context.tsx +465 -0
- package/components/detail-page/detail-page-icons-panel.tsx +251 -0
- package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
- package/components/detail-page/detail-page-layers-panel.tsx +618 -0
- package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
- package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
- package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
- package/components/detail-page/detail-page-pages-panel.tsx +299 -0
- package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
- package/components/detail-page/detail-page-photos-panel.tsx +257 -0
- package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
- package/components/detail-page/detail-page-qr-panel.tsx +137 -0
- package/components/detail-page/detail-page-references-panel.tsx +166 -0
- package/components/detail-page/detail-page-selection.ts +44 -0
- package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
- package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
- package/components/detail-page/detail-page-tables-panel.tsx +106 -0
- package/components/detail-page/detail-page-text-panel.tsx +179 -0
- package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
- package/components/detail-page/edit-quota-ui.tsx +150 -0
- package/components/detail-page/editor-history.ts +56 -0
- package/components/detail-page/editor-hotkeys.tsx +165 -0
- package/components/detail-page/element-recents-strip.tsx +105 -0
- package/components/detail-page/element-rects.ts +82 -0
- package/components/detail-page/fill-control.tsx +261 -0
- package/components/detail-page/find-replace-panel.tsx +252 -0
- package/components/detail-page/generation-progress-fill.tsx +57 -0
- package/components/detail-page/gif-animator.tsx +184 -0
- package/components/detail-page/gif-effect-picker.tsx +136 -0
- package/components/detail-page/group-drill-in.tsx +219 -0
- package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
- package/components/detail-page/hover-highlight-overlay.tsx +111 -0
- package/components/detail-page/hovered-layer.ts +34 -0
- package/components/detail-page/inspector-controls.tsx +175 -0
- package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
- package/components/detail-page/nested-selection-overlay.tsx +364 -0
- package/components/detail-page/panel-search-input.tsx +49 -0
- package/components/detail-page/prompt-edit-panel.tsx +146 -0
- package/components/detail-page/section-height-handle.tsx +276 -0
- package/components/detail-page/section-reauthor-controller.tsx +210 -0
- package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
- package/components/detail-page/spec-resize-absorber.tsx +149 -0
- package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
- package/components/detail-page/table-canvas-overlay.tsx +707 -0
- package/components/detail-page/table-inspector.tsx +586 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/command.tsx +175 -0
- package/components/ui/dialog.tsx +167 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/select.tsx +190 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/tooltip.tsx +57 -0
- package/config/detail-page-bundle-font-tags.json +39 -0
- package/config/detail-page-fonts.json +373 -0
- package/index.ts +24 -0
- package/lib/cardnews/font-loader.ts +306 -0
- package/lib/detail-page/animation-sniff.ts +101 -0
- package/lib/detail-page/asset-bytes-url.ts +70 -0
- package/lib/detail-page/basic-shapes.ts +603 -0
- package/lib/detail-page/canvas-menu.ts +182 -0
- package/lib/detail-page/chart/defaults.ts +283 -0
- package/lib/detail-page/chart/format.ts +66 -0
- package/lib/detail-page/chart/normalize.ts +149 -0
- package/lib/detail-page/chart/palette.ts +85 -0
- package/lib/detail-page/chart/paste.ts +82 -0
- package/lib/detail-page/chart/preview.ts +78 -0
- package/lib/detail-page/chart/render.ts +188 -0
- package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
- package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
- package/lib/detail-page/chart/renderers/donut.ts +186 -0
- package/lib/detail-page/chart/renderers/gauge.ts +96 -0
- package/lib/detail-page/chart/renderers/line.ts +143 -0
- package/lib/detail-page/chart/renderers/shared.ts +263 -0
- package/lib/detail-page/chart/renderers/stack.ts +121 -0
- package/lib/detail-page/chart/sync.ts +92 -0
- package/lib/detail-page/chart/types.ts +145 -0
- package/lib/detail-page/css-color.ts +34 -0
- package/lib/detail-page/data-gif-payload.ts +167 -0
- package/lib/detail-page/design-reference.ts +244 -0
- package/lib/detail-page/distribute.ts +89 -0
- package/lib/detail-page/editor-routes.ts +10 -0
- package/lib/detail-page/element-recents.ts +167 -0
- package/lib/detail-page/find-replace.ts +120 -0
- package/lib/detail-page/format-painter.ts +159 -0
- package/lib/detail-page/gif-frames.ts +216 -0
- package/lib/detail-page/gif-timeline.ts +65 -0
- package/lib/detail-page/group-action.ts +62 -0
- package/lib/detail-page/hangul-roman.ts +58 -0
- package/lib/detail-page/icon-curated.ts +290 -0
- package/lib/detail-page/icon-fuzzy.ts +250 -0
- package/lib/detail-page/icon-keywords.generated.ts +3766 -0
- package/lib/detail-page/icon-keywords.ko.ts +524 -0
- package/lib/detail-page/icon-search.ts +257 -0
- package/lib/detail-page/icons.ts +67 -0
- package/lib/detail-page/image-credit.ts +121 -0
- package/lib/detail-page/image-data-uri.ts +29 -0
- package/lib/detail-page/insert-image.ts +75 -0
- package/lib/detail-page/insert-shape.ts +49 -0
- package/lib/detail-page/korean-dictionary.ts +187 -0
- package/lib/detail-page/layer-move.ts +257 -0
- package/lib/detail-page/new-product-draft-store.ts +105 -0
- package/lib/detail-page/new-product-draft.ts +283 -0
- package/lib/detail-page/personal-images-refresh.ts +30 -0
- package/lib/detail-page/product-source.ts +587 -0
- package/lib/detail-page/qr-code.ts +214 -0
- package/lib/detail-page/reference-image.ts +103 -0
- package/lib/detail-page/reference-upload.ts +181 -0
- package/lib/detail-page/replace-page.ts +35 -0
- package/lib/detail-page/replace-with-gif.ts +251 -0
- package/lib/detail-page/section-height.ts +131 -0
- package/lib/detail-page/section-reauthor-bus.ts +61 -0
- package/lib/detail-page/shape-to-image.ts +219 -0
- package/lib/detail-page/spec-group/clipboard.ts +35 -0
- package/lib/detail-page/spec-group/sync.ts +377 -0
- package/lib/detail-page/spec-group/text-metrics.ts +48 -0
- package/lib/detail-page/stock-photos.ts +88 -0
- package/lib/detail-page/svg-colors.ts +74 -0
- package/lib/detail-page/table/defaults.ts +344 -0
- package/lib/detail-page/table/edit.ts +312 -0
- package/lib/detail-page/table/harvest.ts +103 -0
- package/lib/detail-page/table/normalize.ts +125 -0
- package/lib/detail-page/table/paste.ts +49 -0
- package/lib/detail-page/table/preview.ts +66 -0
- package/lib/detail-page/table/render.ts +388 -0
- package/lib/detail-page/table/sync.ts +107 -0
- package/lib/detail-page/table/types.ts +102 -0
- package/lib/detail-page/text-gif-layout.ts +245 -0
- package/lib/detail-page/text-gif-payload.ts +67 -0
- package/lib/detail-page/text-presets.ts +230 -0
- package/lib/detail-page/use-fake-progress.ts +67 -0
- package/lib/detail-page/z-order.ts +82 -0
- package/lib/detail-page-canvas/bubble-path.ts +190 -0
- package/lib/detail-page-canvas/canvas-key.ts +22 -0
- package/lib/detail-page-canvas/clip-rect.ts +65 -0
- package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
- package/lib/detail-page-canvas/editor-fonts.ts +161 -0
- package/lib/detail-page-canvas/export/ai.ts +413 -0
- package/lib/detail-page-canvas/export/color.ts +35 -0
- package/lib/detail-page-canvas/export/document-model.ts +78 -0
- package/lib/detail-page-canvas/export/export-files.ts +290 -0
- package/lib/detail-page-canvas/export/font-subset.ts +264 -0
- package/lib/detail-page-canvas/export/gif-export.ts +310 -0
- package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
- package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
- package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
- package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
- package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
- package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
- package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
- package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
- package/lib/detail-page-canvas/export/psd.ts +354 -0
- package/lib/detail-page-canvas/export/raster.ts +255 -0
- package/lib/detail-page-canvas/export/svg.ts +479 -0
- package/lib/detail-page-canvas/export/text-layout.ts +142 -0
- package/lib/detail-page-canvas/font-catalog.ts +177 -0
- package/lib/detail-page-canvas/font-tags.ts +47 -0
- package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
- package/lib/detail-page-canvas/konva-fallback.ts +477 -0
- package/lib/detail-page-canvas/text-background-plan.ts +124 -0
- package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
- package/lib/utils.ts +10 -0
- package/package.json +68 -0
- package/types/commerce.ts +171 -0
- package/types/detail-page-api.ts +200 -0
- package/types/detail-page-canvas.ts +20 -0
- package/types/fontkit.d.ts +42 -0
- package/types/gifenc.d.ts +51 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { BarChart3, QrCode, Shapes, Smile, Sparkles, Table, type LucideIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { DetailPageShapesPanel } from "./detail-page-shapes-panel";
|
|
8
|
+
import { DetailPageDecorationsPanel } from "./detail-page-decorations-panel";
|
|
9
|
+
import { DetailPageChartsPanel } from "./detail-page-charts-panel";
|
|
10
|
+
import { DetailPageTablesPanel } from "./detail-page-tables-panel";
|
|
11
|
+
import { DetailPageIconsPanel } from "./detail-page-icons-panel";
|
|
12
|
+
import { DetailPageQrPanel } from "./detail-page-qr-panel";
|
|
13
|
+
import { ElementRecentsStrip } from "./element-recents-strip";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* "요소" 패널 — 도형·아이콘·QR·장식·차트·표를 한 탭 안에 접어 넣고, 위쪽 그룹 바에서 가른다.
|
|
17
|
+
*
|
|
18
|
+
* 레일이 14탭까지 늘어나 포화였다. 넣을 것이 아이콘·프레임까지 더 붙으면 레일 자체가
|
|
19
|
+
* 스크롤되어 "어디에 뭐가 있는지"가 무너진다. 그래서 **삽입 대상이 같은 성격인 것**
|
|
20
|
+
* (클릭 한 번으로 캔버스에 놓이는 벡터/데이터 요소)을 한 서랍으로 모은다. 사진·텍스트는
|
|
21
|
+
* 성격이 달라 그대로 둔다 — 사진은 검색·업로드가 붙고 텍스트는 스타일 프리셋이다.
|
|
22
|
+
*
|
|
23
|
+
* 맨 위의 "즐겨찾기 · 최근" 스트립은 그룹과 무관하게 항상 같은 자리에 있다 — 상세페이지는
|
|
24
|
+
* 같은 배지·아이콘을 20섹션 내내 반복해서 넣으므로 **검색의 절반은 다시 안 찾는 것**이다.
|
|
25
|
+
* 탭이 아니라 스트립인 이유는 레일 상한(12)이다.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type ElementsGroupId =
|
|
29
|
+
| "shapes"
|
|
30
|
+
| "icons"
|
|
31
|
+
| "qr"
|
|
32
|
+
| "decorations"
|
|
33
|
+
| "charts"
|
|
34
|
+
| "tables";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 그룹 바는 3열 두 줄이다. 순서가 곧 자리라서 **도형 바로 아래 칸이 장식**이 된다 —
|
|
38
|
+
* 둘은 같은 열에 세로로 붙어 있어야 "도형에서 갈라져 나온 것"으로 읽힌다.
|
|
39
|
+
*/
|
|
40
|
+
const GROUPS: ReadonlyArray<{
|
|
41
|
+
id: ElementsGroupId;
|
|
42
|
+
labelKey: string;
|
|
43
|
+
Icon: LucideIcon;
|
|
44
|
+
}> = [
|
|
45
|
+
{ id: "shapes", labelKey: "detailPage.sidebar.shapes", Icon: Shapes },
|
|
46
|
+
{ id: "icons", labelKey: "detailPage.sidebar.icons", Icon: Smile },
|
|
47
|
+
{ id: "qr", labelKey: "detailPage.sidebar.qr", Icon: QrCode },
|
|
48
|
+
{ id: "decorations", labelKey: "detailPage.sidebar.decorations", Icon: Sparkles },
|
|
49
|
+
{ id: "charts", labelKey: "detailPage.sidebar.charts", Icon: BarChart3 },
|
|
50
|
+
{ id: "tables", labelKey: "detailPage.sidebar.tables", Icon: Table },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 마지막으로 본 그룹. 모듈 전역인 이유는 껍데기가 **열린 패널 하나만 렌더**하기
|
|
55
|
+
* 때문이다 — 다른 탭에 갔다 오면 이 컴포넌트는 언마운트됐다가 새로 뜬다. 탭이 셋으로
|
|
56
|
+
* 갈려 있을 때는 레일이 이 기억을 대신했으니, 접은 뒤에도 그 감각은 남겨 둔다.
|
|
57
|
+
* 세션 안에서만 살면 충분해서 저장소까지 가지 않는다.
|
|
58
|
+
*/
|
|
59
|
+
let lastGroup: ElementsGroupId = "shapes";
|
|
60
|
+
|
|
61
|
+
export function DetailPageElementsPanel({
|
|
62
|
+
store,
|
|
63
|
+
defaultGroup,
|
|
64
|
+
}: {
|
|
65
|
+
store: unknown;
|
|
66
|
+
/** 처음 열 그룹. 안 주면 이 세션에서 마지막으로 봤던 그룹으로 돌아간다. */
|
|
67
|
+
defaultGroup?: ElementsGroupId;
|
|
68
|
+
}) {
|
|
69
|
+
const { t } = useTranslation("branding");
|
|
70
|
+
const [group, setGroup] = useState<ElementsGroupId>(defaultGroup ?? lastGroup);
|
|
71
|
+
|
|
72
|
+
const select = useCallback((next: ElementsGroupId) => {
|
|
73
|
+
lastGroup = next;
|
|
74
|
+
setGroup(next);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex h-full flex-col">
|
|
79
|
+
<ElementRecentsStrip store={store} />
|
|
80
|
+
<div
|
|
81
|
+
role="tablist"
|
|
82
|
+
aria-label={t("detailPage.sidebar.elements")}
|
|
83
|
+
className="grid shrink-0 grid-cols-3 gap-0.5 border-b border-neutral-200 p-2"
|
|
84
|
+
>
|
|
85
|
+
{GROUPS.map(({ id, labelKey, Icon }) => {
|
|
86
|
+
const active = id === group;
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
key={id}
|
|
90
|
+
type="button"
|
|
91
|
+
role="tab"
|
|
92
|
+
aria-selected={active}
|
|
93
|
+
onClick={() => select(id)}
|
|
94
|
+
className={`flex flex-col items-center justify-center gap-0.5 rounded-md px-1 py-1.5 text-[11px] font-medium transition-colors ${
|
|
95
|
+
active
|
|
96
|
+
? "bg-blue-50 text-blue-700"
|
|
97
|
+
: "text-neutral-500 hover:bg-neutral-100"
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<Icon aria-hidden="true" size={14} />
|
|
101
|
+
{t(labelKey)}
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
{/* 그룹 패널은 저마다 h-full 스크롤러라 남은 높이를 통째로 넘긴다. */}
|
|
107
|
+
<div className="min-h-0 flex-1">
|
|
108
|
+
{group === "shapes" ? <DetailPageShapesPanel store={store} /> : null}
|
|
109
|
+
{group === "icons" ? <DetailPageIconsPanel store={store} /> : null}
|
|
110
|
+
{group === "qr" ? <DetailPageQrPanel store={store} /> : null}
|
|
111
|
+
{group === "decorations" ? <DetailPageDecorationsPanel store={store} /> : null}
|
|
112
|
+
{group === "charts" ? <DetailPageChartsPanel store={store} /> : null}
|
|
113
|
+
{group === "tables" ? <DetailPageTablesPanel store={store} /> : null}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { Check, ChevronDown, LoaderCircle } from "lucide-react";
|
|
6
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
7
|
+
import { useTranslation } from "react-i18next";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Command,
|
|
11
|
+
CommandEmpty,
|
|
12
|
+
CommandGroup,
|
|
13
|
+
CommandInput,
|
|
14
|
+
CommandItem,
|
|
15
|
+
CommandList,
|
|
16
|
+
} from "../ui/command";
|
|
17
|
+
import {
|
|
18
|
+
EDITOR_BUNDLE_FONTS,
|
|
19
|
+
EDITOR_CATALOG_FONTS,
|
|
20
|
+
filterFontsByTags,
|
|
21
|
+
getEditorFont,
|
|
22
|
+
type EditorFont,
|
|
23
|
+
} from "../../lib/detail-page-canvas/editor-fonts";
|
|
24
|
+
import { FONT_TAGS, type FontTag } from "../../lib/detail-page-canvas/font-tags";
|
|
25
|
+
|
|
26
|
+
export function DetailPageFontPicker({
|
|
27
|
+
value,
|
|
28
|
+
documentFamilies,
|
|
29
|
+
onSelect,
|
|
30
|
+
}: {
|
|
31
|
+
value: string;
|
|
32
|
+
documentFamilies: string[];
|
|
33
|
+
onSelect: (family: string) => Promise<void>;
|
|
34
|
+
}) {
|
|
35
|
+
const { t } = useTranslation("branding");
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [loading, setLoading] = useState<string | null>(null);
|
|
38
|
+
const [error, setError] = useState(false);
|
|
39
|
+
const [tags, setTags] = useState<FontTag[]>([]);
|
|
40
|
+
const selected = getEditorFont(value);
|
|
41
|
+
const catalogFonts = filterFontsByTags(EDITOR_CATALOG_FONTS, tags);
|
|
42
|
+
const bundleFonts = filterFontsByTags(EDITOR_BUNDLE_FONTS, tags);
|
|
43
|
+
// Document fonts carry no tags, so any active chip is a statement that they are
|
|
44
|
+
// not what the user is looking for.
|
|
45
|
+
const legacyFamilies = tags.length
|
|
46
|
+
? []
|
|
47
|
+
: documentFamilies.filter((family) => !getEditorFont(family));
|
|
48
|
+
|
|
49
|
+
const toggleTag = (tag: FontTag) => {
|
|
50
|
+
setTags((current) =>
|
|
51
|
+
current.includes(tag)
|
|
52
|
+
? current.filter((candidate) => candidate !== tag)
|
|
53
|
+
: [...current, tag],
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const select = async (family: string) => {
|
|
58
|
+
setLoading(family);
|
|
59
|
+
setError(false);
|
|
60
|
+
try {
|
|
61
|
+
await onSelect(family);
|
|
62
|
+
setOpen(false);
|
|
63
|
+
} catch (fontError) {
|
|
64
|
+
console.error(`Failed to load detail-page font "${family}"`, fontError);
|
|
65
|
+
setError(true);
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(null);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const renderFont = (font: EditorFont) => (
|
|
72
|
+
<CommandItem
|
|
73
|
+
key={`${font.source}:${font.id}`}
|
|
74
|
+
value={`${font.label} ${font.labelEn} ${font.family}`}
|
|
75
|
+
onSelect={() => void select(font.family)}
|
|
76
|
+
disabled={loading !== null}
|
|
77
|
+
className="group min-h-[56px] gap-2 px-2 py-1"
|
|
78
|
+
>
|
|
79
|
+
<span className="flex h-11 min-w-0 flex-1 items-center overflow-hidden">
|
|
80
|
+
<Image
|
|
81
|
+
src={font.previewSrc}
|
|
82
|
+
alt={font.label}
|
|
83
|
+
width={560}
|
|
84
|
+
height={88}
|
|
85
|
+
className="h-auto w-full object-contain object-left"
|
|
86
|
+
unoptimized
|
|
87
|
+
/>
|
|
88
|
+
</span>
|
|
89
|
+
{font.latinOnly ? (
|
|
90
|
+
<span className="shrink-0 rounded border border-neutral-200 px-1 py-0.5 text-[10px] leading-none text-neutral-500">
|
|
91
|
+
{t("detailPage.properties.fontLatinOnly")}
|
|
92
|
+
</span>
|
|
93
|
+
) : null}
|
|
94
|
+
{loading === font.family ? (
|
|
95
|
+
<LoaderCircle
|
|
96
|
+
aria-label={t("detailPage.properties.fontLoading")}
|
|
97
|
+
className="shrink-0 animate-spin text-neutral-500"
|
|
98
|
+
size={15}
|
|
99
|
+
/>
|
|
100
|
+
) : value === font.family ? (
|
|
101
|
+
<Check aria-hidden="true" className="shrink-0" size={15} />
|
|
102
|
+
) : null}
|
|
103
|
+
</CommandItem>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<PopoverPrimitive.Root
|
|
108
|
+
open={open}
|
|
109
|
+
onOpenChange={(next) => {
|
|
110
|
+
setOpen(next);
|
|
111
|
+
if (!next) setError(false);
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<PopoverPrimitive.Trigger asChild>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
aria-label={t("detailPage.properties.chooseFont")}
|
|
118
|
+
aria-expanded={open}
|
|
119
|
+
className="flex min-w-0 items-center justify-between gap-2 rounded-md border border-neutral-200 bg-white px-2 py-1.5 text-left text-sm text-neutral-900 outline-none hover:bg-neutral-50 focus:border-neutral-400"
|
|
120
|
+
>
|
|
121
|
+
<span className="min-w-0 truncate">{selected?.label ?? value}</span>
|
|
122
|
+
<ChevronDown aria-hidden="true" className="shrink-0 text-neutral-400" size={14} />
|
|
123
|
+
</button>
|
|
124
|
+
</PopoverPrimitive.Trigger>
|
|
125
|
+
<PopoverPrimitive.Portal>
|
|
126
|
+
<PopoverPrimitive.Content
|
|
127
|
+
side="left"
|
|
128
|
+
align="start"
|
|
129
|
+
sideOffset={8}
|
|
130
|
+
collisionPadding={12}
|
|
131
|
+
className="z-[120] w-[340px] overflow-hidden rounded-lg border border-neutral-200 bg-white shadow-[0_10px_32px_rgba(0,0,0,0.14)]"
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
role="group"
|
|
135
|
+
aria-label={t("detailPage.properties.fontTagFilter")}
|
|
136
|
+
className="flex flex-wrap gap-1 border-b border-neutral-100 px-2 py-2"
|
|
137
|
+
>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
aria-pressed={tags.length === 0}
|
|
141
|
+
onClick={() => setTags([])}
|
|
142
|
+
className={`rounded-full border px-2 py-0.5 text-[11px] leading-4 transition-colors ${
|
|
143
|
+
tags.length === 0
|
|
144
|
+
? "border-neutral-900 bg-neutral-900 text-white"
|
|
145
|
+
: "border-neutral-200 text-neutral-600 hover:bg-neutral-50"
|
|
146
|
+
}`}
|
|
147
|
+
>
|
|
148
|
+
{t("detailPage.properties.fontTagAll")}
|
|
149
|
+
</button>
|
|
150
|
+
{FONT_TAGS.map((tag) => (
|
|
151
|
+
<button
|
|
152
|
+
key={tag}
|
|
153
|
+
type="button"
|
|
154
|
+
aria-pressed={tags.includes(tag)}
|
|
155
|
+
onClick={() => toggleTag(tag)}
|
|
156
|
+
className={`rounded-full border px-2 py-0.5 text-[11px] leading-4 transition-colors ${
|
|
157
|
+
tags.includes(tag)
|
|
158
|
+
? "border-neutral-900 bg-neutral-900 text-white"
|
|
159
|
+
: "border-neutral-200 text-neutral-600 hover:bg-neutral-50"
|
|
160
|
+
}`}
|
|
161
|
+
>
|
|
162
|
+
{t(`detailPage.fontTags.${tag}`)}
|
|
163
|
+
</button>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
<Command>
|
|
167
|
+
<CommandInput
|
|
168
|
+
autoFocus
|
|
169
|
+
placeholder={t("detailPage.properties.fontSearch")}
|
|
170
|
+
aria-label={t("detailPage.properties.fontSearch")}
|
|
171
|
+
/>
|
|
172
|
+
<CommandList className="max-h-[420px]">
|
|
173
|
+
<CommandEmpty>
|
|
174
|
+
{t("detailPage.properties.fontEmpty")}
|
|
175
|
+
</CommandEmpty>
|
|
176
|
+
{catalogFonts.length ? (
|
|
177
|
+
<CommandGroup heading={t("detailPage.properties.fontCatalog")}>
|
|
178
|
+
{catalogFonts.map(renderFont)}
|
|
179
|
+
</CommandGroup>
|
|
180
|
+
) : null}
|
|
181
|
+
{bundleFonts.length ? (
|
|
182
|
+
<CommandGroup heading={t("detailPage.properties.fontBundle")}>
|
|
183
|
+
{bundleFonts.map(renderFont)}
|
|
184
|
+
</CommandGroup>
|
|
185
|
+
) : null}
|
|
186
|
+
{legacyFamilies.length ? (
|
|
187
|
+
<CommandGroup heading={t("detailPage.properties.documentFonts")}>
|
|
188
|
+
{legacyFamilies.map((family) => (
|
|
189
|
+
<CommandItem
|
|
190
|
+
key={family}
|
|
191
|
+
value={family}
|
|
192
|
+
onSelect={() => void select(family)}
|
|
193
|
+
disabled={loading !== null}
|
|
194
|
+
className="gap-2 px-3 py-2.5"
|
|
195
|
+
>
|
|
196
|
+
<span className="min-w-0 flex-1 truncate">{family}</span>
|
|
197
|
+
{value === family ? <Check aria-hidden="true" size={15} /> : null}
|
|
198
|
+
</CommandItem>
|
|
199
|
+
))}
|
|
200
|
+
</CommandGroup>
|
|
201
|
+
) : null}
|
|
202
|
+
</CommandList>
|
|
203
|
+
</Command>
|
|
204
|
+
{error ? (
|
|
205
|
+
<p role="alert" className="border-t border-red-100 bg-red-50 px-3 py-2 text-xs text-red-600">
|
|
206
|
+
{t("detailPage.properties.fontLoadFailed")}
|
|
207
|
+
</p>
|
|
208
|
+
) : null}
|
|
209
|
+
{selected ? (
|
|
210
|
+
<div className="border-t border-neutral-100 px-3 py-2 text-[11px] leading-4 text-neutral-500">
|
|
211
|
+
<a
|
|
212
|
+
href={selected.licenseUrl}
|
|
213
|
+
target="_blank"
|
|
214
|
+
rel="noreferrer"
|
|
215
|
+
className="font-medium text-neutral-700 underline decoration-neutral-300 underline-offset-2"
|
|
216
|
+
>
|
|
217
|
+
{t("detailPage.properties.fontLicense")}: {selected.licenseName}
|
|
218
|
+
</a>
|
|
219
|
+
{selected.licenseNoteKey ? (
|
|
220
|
+
<span className="ml-1">
|
|
221
|
+
· {t(`detailPage.fontLicenseNotes.${selected.licenseNoteKey}`)}
|
|
222
|
+
</span>
|
|
223
|
+
) : null}
|
|
224
|
+
</div>
|
|
225
|
+
) : null}
|
|
226
|
+
<PopoverPrimitive.Arrow className="fill-white" />
|
|
227
|
+
</PopoverPrimitive.Content>
|
|
228
|
+
</PopoverPrimitive.Portal>
|
|
229
|
+
</PopoverPrimitive.Root>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { observer } from "./canvas-observer";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { Redo2, Undo2 } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
safeRedo,
|
|
9
|
+
safeUndo,
|
|
10
|
+
type HistoryStore,
|
|
11
|
+
} from "./editor-history";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 되돌리기 / 다시 실행 버튼. Canvas store의 관측 가능한 ``history.canUndo`` /
|
|
15
|
+
* ``history.canRedo``를 읽어 비활성 상태를 반응형으로 갱신하고, 클릭 시
|
|
16
|
+
* 되돌리기/다시 실행을 실행한다. 실제 호출은 ``safeUndo`` / ``safeRedo``를 거치는데,
|
|
17
|
+
* 선택이 살아 있는 채로 undo하면 스톡 편집기가 MST assertion으로 죽기 때문이다
|
|
18
|
+
* (editor-history.ts 참고). 단축키(⌘/Ctrl+Z 등)는 ``EditorHotkeys``가 처리한다.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type HistoryStoreLike = {
|
|
22
|
+
history: {
|
|
23
|
+
canUndo: boolean;
|
|
24
|
+
canRedo: boolean;
|
|
25
|
+
undo: () => void;
|
|
26
|
+
redo: () => void;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function HistoryButton({
|
|
31
|
+
label,
|
|
32
|
+
onClick,
|
|
33
|
+
disabled,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
label: string;
|
|
37
|
+
onClick: () => void;
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
title={label}
|
|
45
|
+
aria-label={label}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
onClick={onClick}
|
|
48
|
+
className={[
|
|
49
|
+
"flex h-9 w-9 items-center justify-center rounded-md transition-colors",
|
|
50
|
+
disabled
|
|
51
|
+
? "cursor-not-allowed text-neutral-300"
|
|
52
|
+
: "text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900",
|
|
53
|
+
].join(" ")}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DetailPageHistoryButtons = observer(function DetailPageHistoryButtons({
|
|
61
|
+
store,
|
|
62
|
+
}: {
|
|
63
|
+
store: unknown;
|
|
64
|
+
}) {
|
|
65
|
+
const { t } = useTranslation("branding");
|
|
66
|
+
const { history } = store as HistoryStoreLike;
|
|
67
|
+
const s = store as HistoryStore;
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex items-center">
|
|
70
|
+
<HistoryButton
|
|
71
|
+
label={t("editor.undo")}
|
|
72
|
+
onClick={() => safeUndo(s)}
|
|
73
|
+
disabled={!history.canUndo}
|
|
74
|
+
>
|
|
75
|
+
<Undo2 size={17} />
|
|
76
|
+
</HistoryButton>
|
|
77
|
+
<HistoryButton
|
|
78
|
+
label={t("editor.redo")}
|
|
79
|
+
onClick={() => safeRedo(s)}
|
|
80
|
+
disabled={!history.canRedo}
|
|
81
|
+
>
|
|
82
|
+
<Redo2 size={17} />
|
|
83
|
+
</HistoryButton>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
DetailPageHistoryButtons.displayName = "DetailPageHistoryButtons";
|