@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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 캔버스에서 고친 칸 글자를 스펙으로 되받는다.
|
|
3
|
+
*
|
|
4
|
+
* 표의 칸은 평범한 ``text`` 자식이라 드릴인하면 그 자리에서 고쳐진다(Canva에서 하듯이).
|
|
5
|
+
* 그런데 다시 그리는 기준은 언제나 스펙이므로, 되받지 않으면 사용자가 **다음에 행 하나
|
|
6
|
+
* 늘리는 순간** 캔버스에서 친 글자가 스펙 값으로 덮인다. 눈에는 편집이 되는 것처럼
|
|
7
|
+
* 보이고 한참 뒤에 사라지는, 가장 나쁜 종류의 손실이다.
|
|
8
|
+
*
|
|
9
|
+
* 그래서 3-way 병합을 한다. 되받기와 패널 편집이 동시에 일어날 수 있기 때문이다 —
|
|
10
|
+
* 예를 들어 사용자가 캔버스에서 1행을 고치고, 우측 패널에서 2행을 고친 뒤 Enter를 치면
|
|
11
|
+
* 그 한 번의 재생성에 둘 다 살아야 한다.
|
|
12
|
+
*
|
|
13
|
+
* base = 그룹에 저장돼 있던 스펙(마지막으로 그린 그림의 근거)
|
|
14
|
+
* canvas = 지금 자식 ``text``에 들어 있는 글자
|
|
15
|
+
* incoming = 이번에 적용하려는 스펙(패널·AI 편집이 만든 것)
|
|
16
|
+
*
|
|
17
|
+
* 칸마다: 캔버스가 base와 다르고 **incoming은 base 그대로**면 캔버스를 택한다. 둘 다
|
|
18
|
+
* 바뀌었으면 incoming이 이긴다 — 방금 명시적으로 지시한 쪽이라서다.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ElementLike } from "../spec-group/sync";
|
|
22
|
+
|
|
23
|
+
import { TABLE_PART, type TableSpec } from "./types";
|
|
24
|
+
|
|
25
|
+
/** ``cell:3:1`` / ``header:2`` → 어느 칸인가. 그 외 부품(바탕·선)은 ``null``. */
|
|
26
|
+
export function parseCellKey(
|
|
27
|
+
key: string,
|
|
28
|
+
): { kind: "cell"; row: number; column: number } | { kind: "header"; column: number } | null {
|
|
29
|
+
const cell = /^cell:(\d+):(\d+)$/.exec(key);
|
|
30
|
+
if (cell) return { kind: "cell", row: Number(cell[1]), column: Number(cell[2]) };
|
|
31
|
+
const header = /^header:(\d+)$/.exec(key);
|
|
32
|
+
if (header) return { kind: "header", column: Number(header[1]) };
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 자식에서 (부품 키, 글자)를 뽑는다. ``text``가 아닌 부품은 글자를 들고 있지 않다. */
|
|
37
|
+
function readCellTexts(children: ElementLike[]): Map<string, string> {
|
|
38
|
+
const out = new Map<string, string>();
|
|
39
|
+
for (const kid of children) {
|
|
40
|
+
if (kid.type !== "text") continue;
|
|
41
|
+
const key = (kid.custom as Record<string, unknown> | undefined)?.[TABLE_PART];
|
|
42
|
+
const value = (kid as unknown as { text?: unknown }).text;
|
|
43
|
+
if (typeof key === "string" && typeof value === "string") out.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 캔버스 편집을 ``incoming``에 얹은 스펙. 바뀐 게 없으면 ``incoming``을 그대로 돌려준다
|
|
50
|
+
* (참조가 같아야 호출부가 "달라졌는가"를 싸게 판단한다).
|
|
51
|
+
*/
|
|
52
|
+
export function mergeCanvasCells(
|
|
53
|
+
base: TableSpec,
|
|
54
|
+
incoming: TableSpec,
|
|
55
|
+
children: ElementLike[],
|
|
56
|
+
): TableSpec {
|
|
57
|
+
const texts = readCellTexts(children);
|
|
58
|
+
if (texts.size === 0) return incoming;
|
|
59
|
+
|
|
60
|
+
let rows: string[][] | null = null;
|
|
61
|
+
let columns: string[] | null = null;
|
|
62
|
+
|
|
63
|
+
for (const [key, canvas] of texts) {
|
|
64
|
+
const at = parseCellKey(key);
|
|
65
|
+
if (!at) continue;
|
|
66
|
+
|
|
67
|
+
if (at.kind === "header") {
|
|
68
|
+
const baseValue = base.data.columns[at.column];
|
|
69
|
+
const incomingValue = incoming.data.columns[at.column];
|
|
70
|
+
if (canvas === baseValue || incomingValue !== baseValue) continue;
|
|
71
|
+
columns = columns ?? [...incoming.data.columns];
|
|
72
|
+
columns[at.column] = canvas;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const baseValue = base.data.rows[at.row]?.[at.column];
|
|
77
|
+
const incomingValue = incoming.data.rows[at.row]?.[at.column];
|
|
78
|
+
// 행이 아직 없는 자리(스펙보다 자식이 앞선 경우)는 건드리지 않는다.
|
|
79
|
+
if (baseValue === undefined || incomingValue === undefined) continue;
|
|
80
|
+
if (canvas === baseValue || incomingValue !== baseValue) continue;
|
|
81
|
+
rows = rows ?? incoming.data.rows.map((cells) => [...cells]);
|
|
82
|
+
rows[at.row][at.column] = canvas;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!rows && !columns) return incoming;
|
|
86
|
+
return {
|
|
87
|
+
...incoming,
|
|
88
|
+
data: {
|
|
89
|
+
columns: columns ?? incoming.data.columns,
|
|
90
|
+
rows: rows ?? incoming.data.rows,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 캔버스 글자만 반영한 스펙(패널 쪽 변경 없이).
|
|
97
|
+
*
|
|
98
|
+
* 오버레이가 행·열을 건드리기 **직전**에 부른다. 행을 끼우면 인덱스가 밀려서 그 뒤로는
|
|
99
|
+
* 3-way 병합이 같은 칸을 못 알아보기 때문이다.
|
|
100
|
+
*/
|
|
101
|
+
export function harvestTableEdits(base: TableSpec, children: ElementLike[]): TableSpec {
|
|
102
|
+
return mergeCanvasCells(base, base, children);
|
|
103
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 스펙을 렌더 가능한 모양으로 맞춘다.
|
|
3
|
+
*
|
|
4
|
+
* **원본 ``spec.data``는 절대 자르지 않는다.** 종류를 ``grid`` → ``keyvalue``로 바꿨다가
|
|
5
|
+
* 되돌리면 3열이 그대로 돌아와야 한다. 줄어드는 건 그리는 부분뿐이다(차트와 같은 규칙).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CellAlign, TableSpec } from "./types";
|
|
9
|
+
|
|
10
|
+
/** 붙여넣기·프롬프트 편집이 함께 지키는 상한. 넘으면 편집기가 아니라 스프레드시트 일이다. */
|
|
11
|
+
export const MAX_ROWS = 40;
|
|
12
|
+
export const MAX_COLUMNS = 8;
|
|
13
|
+
|
|
14
|
+
/** ``keyvalue``는 이름/값 두 칸이다. */
|
|
15
|
+
const KEYVALUE_COLUMNS = 2;
|
|
16
|
+
|
|
17
|
+
export type ResolvedTable = {
|
|
18
|
+
/** 실제로 그릴 열 수. */
|
|
19
|
+
columnCount: number;
|
|
20
|
+
/** 머리글 행을 그리는가. */
|
|
21
|
+
showHeader: boolean;
|
|
22
|
+
/** 머리글 칸 글자(그릴 때만). */
|
|
23
|
+
header: string[];
|
|
24
|
+
/** 열 수에 맞춰 채우고 자른 행들(**렌더용 사본**). */
|
|
25
|
+
rows: string[][];
|
|
26
|
+
/** 열별 정렬(열 수만큼 채워짐). */
|
|
27
|
+
align: CellAlign[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 열 인덱스의 기본 정렬. 첫 칸은 이름이라 왼쪽, 나머지도 왼쪽에서 시작한다. */
|
|
31
|
+
function defaultAlign(): CellAlign {
|
|
32
|
+
return "left";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveTable(spec: TableSpec): ResolvedTable {
|
|
36
|
+
const declared = spec.data.columns.length;
|
|
37
|
+
const widest = spec.data.rows.reduce((max, row) => Math.max(max, row.length), 0);
|
|
38
|
+
const columnCount =
|
|
39
|
+
spec.kind === "keyvalue"
|
|
40
|
+
? KEYVALUE_COLUMNS
|
|
41
|
+
: Math.min(MAX_COLUMNS, Math.max(1, declared || widest || 1));
|
|
42
|
+
|
|
43
|
+
const rows = spec.data.rows.slice(0, MAX_ROWS).map((row) => {
|
|
44
|
+
const cells = row.slice(0, columnCount).map((cell) => String(cell ?? ""));
|
|
45
|
+
while (cells.length < columnCount) cells.push("");
|
|
46
|
+
return cells;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const align: CellAlign[] = [];
|
|
50
|
+
for (let i = 0; i < columnCount; i += 1) {
|
|
51
|
+
align.push(spec.options.align[i] ?? defaultAlign());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 머리글은 grid에서만. keyvalue는 이름 칸이 이미 각 행의 머리글 노릇을 한다.
|
|
55
|
+
const showHeader = spec.kind === "grid" && spec.options.headerRow;
|
|
56
|
+
const header: string[] = [];
|
|
57
|
+
if (showHeader) {
|
|
58
|
+
for (let i = 0; i < columnCount; i += 1) {
|
|
59
|
+
header.push(String(spec.data.columns[i] ?? ""));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { columnCount, showHeader, header, rows, align };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 열 폭. 첫 칸만 고정폭을 가질 수 있고 나머지는 남는 폭을 똑같이 나눈다.
|
|
68
|
+
*
|
|
69
|
+
* 브랜드 두 곳이 첫 칸을 190px로 고정해 쓴다 — 이름 칸이 들쭉날쭉하면 값이 세로로 안 맞아
|
|
70
|
+
* 표가 무너져 보인다. 값 칸까지 폭을 스펙에 담지는 않는다(글자를 고칠 때마다 사용자가
|
|
71
|
+
* 폭을 다시 만져야 한다).
|
|
72
|
+
*/
|
|
73
|
+
/** 열 하나가 가질 수 있는 최소 폭. 이보다 좁으면 여백만 남아 글자가 안 보인다. */
|
|
74
|
+
export const MIN_COLUMN_WIDTH = 40;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 선언된 폭을 프레임 폭에 맞춰 비례 정규화한다.
|
|
78
|
+
*
|
|
79
|
+
* 비율만 지키므로 표를 리사이즈해도, 프레임이 바뀌어도 사용자가 잡아 둔 배분이 남는다.
|
|
80
|
+
* 최소 폭에 걸린 열은 그 값으로 고정하고 **남은 폭만** 나머지가 나눠 갖는다 — 안 그러면
|
|
81
|
+
* 정규화가 최소 폭을 다시 뭉개서 좁은 열이 사라진다.
|
|
82
|
+
*/
|
|
83
|
+
export function normalizeColumnWidths(declared: number[], total: number): number[] {
|
|
84
|
+
const count = declared.length;
|
|
85
|
+
if (count === 0) return [];
|
|
86
|
+
// 다 최소 폭을 줘도 넘치면 배분이 의미가 없다 — 균등하게 나눈다.
|
|
87
|
+
if (total < MIN_COLUMN_WIDTH * count) {
|
|
88
|
+
return Array.from({ length: count }, () => total / count);
|
|
89
|
+
}
|
|
90
|
+
const positive = declared.map((w) => (Number.isFinite(w) && w > 0 ? w : 0));
|
|
91
|
+
const sum = positive.reduce((a, b) => a + b, 0);
|
|
92
|
+
if (sum <= 0) return Array.from({ length: count }, () => total / count);
|
|
93
|
+
|
|
94
|
+
const scaled = positive.map((w) => (w / sum) * total);
|
|
95
|
+
const pinned = scaled.map((w) => w < MIN_COLUMN_WIDTH);
|
|
96
|
+
if (!pinned.some(Boolean)) return scaled;
|
|
97
|
+
|
|
98
|
+
const fixed = pinned.reduce((acc, isPinned) => acc + (isPinned ? MIN_COLUMN_WIDTH : 0), 0);
|
|
99
|
+
const rest = total - fixed;
|
|
100
|
+
const restSum = scaled.reduce((acc, w, i) => acc + (pinned[i] ? 0 : w), 0);
|
|
101
|
+
if (restSum <= 0) return Array.from({ length: count }, () => total / count);
|
|
102
|
+
return scaled.map((w, i) => (pinned[i] ? MIN_COLUMN_WIDTH : (w / restSum) * rest));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function columnWidths(spec: TableSpec, resolved: ResolvedTable): number[] {
|
|
106
|
+
const total = Math.max(1, spec.frame.width);
|
|
107
|
+
const { columnCount } = resolved;
|
|
108
|
+
|
|
109
|
+
// 사용자가 잡아 둔 배분이 있으면 그게 우선이다. 열 수가 안 맞으면(종류를 바꿨을 때)
|
|
110
|
+
// 조용히 자동으로 돌아간다 — 엉뚱한 열에 폭이 남는 것보다 낫다.
|
|
111
|
+
const declared = spec.style.columnWidths;
|
|
112
|
+
if (declared && declared.length === columnCount) {
|
|
113
|
+
return normalizeColumnWidths(declared, total);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const first = spec.style.firstWidth;
|
|
117
|
+
if (first === null || columnCount <= 1) {
|
|
118
|
+
const even = total / columnCount;
|
|
119
|
+
return Array.from({ length: columnCount }, () => even);
|
|
120
|
+
}
|
|
121
|
+
// 첫 칸이 표를 다 먹지 않게 막는다(값 칸이 사라지면 표가 아니다).
|
|
122
|
+
const head = Math.max(40, Math.min(first, total - 60));
|
|
123
|
+
const rest = (total - head) / (columnCount - 1);
|
|
124
|
+
return [head, ...Array.from({ length: columnCount - 1 }, () => rest)];
|
|
125
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 붙여넣은 텍스트를 표 데이터로.
|
|
3
|
+
*
|
|
4
|
+
* 표는 값을 숫자로 읽을 필요가 없다 — 칸에 들어간 글자를 그대로 쓴다("18mm · 310g",
|
|
5
|
+
* "사철 양장 · 180° 평면 펼침" 같은 값이 실제로 많다).
|
|
6
|
+
*
|
|
7
|
+
* 머리글 판정은 차트와 같은 규칙이다: 첫 행의 값 칸에서 숫자를 하나도 못 읽으면 머리글.
|
|
8
|
+
* 스펙표처럼 첫 행부터 데이터인 경우가 흔해서, 머리글이 아니라고 보면 **행을 안 버린다**.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseGrid } from "../spec-group/clipboard";
|
|
12
|
+
import { parseChartNumber } from "../chart/format";
|
|
13
|
+
|
|
14
|
+
import { MAX_COLUMNS, MAX_ROWS } from "./normalize";
|
|
15
|
+
import type { TableData } from "./types";
|
|
16
|
+
|
|
17
|
+
export function parseTableGrid(text: string): TableData | null {
|
|
18
|
+
const grid = parseGrid(text ?? "");
|
|
19
|
+
if (grid.length === 0) return null;
|
|
20
|
+
|
|
21
|
+
const columnCount = Math.min(
|
|
22
|
+
MAX_COLUMNS,
|
|
23
|
+
grid.reduce((most, row) => Math.max(most, row.length), 0),
|
|
24
|
+
);
|
|
25
|
+
if (columnCount === 0) return null;
|
|
26
|
+
|
|
27
|
+
const pad = (row: string[]): string[] => {
|
|
28
|
+
const cells = row.slice(0, columnCount);
|
|
29
|
+
while (cells.length < columnCount) cells.push("");
|
|
30
|
+
return cells;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const head = pad(grid[0]);
|
|
34
|
+
// 값 칸(1열 이후)에 숫자가 하나도 없고 아래에 행이 더 있으면 머리글로 본다.
|
|
35
|
+
const headLooksLikeHeader =
|
|
36
|
+
grid.length > 1 &&
|
|
37
|
+
head.slice(1).every((cell) => cell === "" || parseChartNumber(cell) === null);
|
|
38
|
+
|
|
39
|
+
const body = (headLooksLikeHeader ? grid.slice(1) : grid)
|
|
40
|
+
.slice(0, MAX_ROWS)
|
|
41
|
+
.map(pad);
|
|
42
|
+
if (body.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
const columns = headLooksLikeHeader
|
|
45
|
+
? head
|
|
46
|
+
: Array.from({ length: columnCount }, (_, i) => (i === 0 ? "항목" : `값 ${i}`));
|
|
47
|
+
|
|
48
|
+
return { columns, rows: body };
|
|
49
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 좌측 패널 썸네일용 SVG.
|
|
3
|
+
*
|
|
4
|
+
* 별도 이미지 자산을 만들지 않고 **같은 렌더러 출력**을 SVG로 옮긴다. 그래서 프리셋을
|
|
5
|
+
* 고치면 썸네일이 저절로 따라오고, 카탈로그와 실제 결과가 어긋날 일이 없다(차트와 같다).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderTable } from "./render";
|
|
9
|
+
import type { TableSpec } from "./types";
|
|
10
|
+
|
|
11
|
+
function escapeXml(text: string): string {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">")
|
|
16
|
+
.replace(/"/g, """);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function num(value: unknown, fallback = 0): number {
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function str(value: unknown, fallback = ""): string {
|
|
24
|
+
return typeof value === "string" ? value : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nodeMarkup(props: Record<string, unknown>): string {
|
|
28
|
+
const x = num(props.x);
|
|
29
|
+
const y = num(props.y);
|
|
30
|
+
if (props.type === "figure") {
|
|
31
|
+
const radius = num(props.cornerRadius);
|
|
32
|
+
const strokeWidth = num(props.strokeWidth);
|
|
33
|
+
const stroke =
|
|
34
|
+
strokeWidth > 0 && typeof props.stroke === "string"
|
|
35
|
+
? ` stroke="${escapeXml(props.stroke)}" stroke-width="${strokeWidth}"`
|
|
36
|
+
: "";
|
|
37
|
+
return `<rect x="${x}" y="${y}" width="${num(props.width)}" height="${num(
|
|
38
|
+
props.height,
|
|
39
|
+
)}" rx="${radius}" ry="${radius}" fill="${escapeXml(
|
|
40
|
+
str(props.fill, "#000"),
|
|
41
|
+
)}"${stroke}/>`;
|
|
42
|
+
}
|
|
43
|
+
if (props.type === "text") {
|
|
44
|
+
const value = str(props.text);
|
|
45
|
+
if (!value) return "";
|
|
46
|
+
const width = num(props.width);
|
|
47
|
+
const size = num(props.fontSize, 12);
|
|
48
|
+
const align = str(props.align, "left");
|
|
49
|
+
const anchor = align === "right" ? "end" : align === "center" ? "middle" : "start";
|
|
50
|
+
const anchorX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
|
|
51
|
+
// SVG는 baseline 기준이라 한 줄 높이의 약 0.85를 내려 앉힌다(썸네일 근사치).
|
|
52
|
+
// 썸네일은 한 줄만 보여 준다 — 접힌 줄까지 그리면 칸 높이 계산을 두 벌 갖게 된다.
|
|
53
|
+
const firstLine = value.split("\n")[0];
|
|
54
|
+
return `<text x="${anchorX}" y="${y + size * 0.85}" font-size="${size}" text-anchor="${anchor}" font-weight="${escapeXml(
|
|
55
|
+
str(props.fontWeight, "400"),
|
|
56
|
+
)}" fill="${escapeXml(str(props.fill, "#000"))}">${escapeXml(firstLine)}</text>`;
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 스펙을 그대로 그린 미리보기 SVG 마크업. */
|
|
62
|
+
export function tablePreviewSvg(spec: TableSpec): string {
|
|
63
|
+
const { nodes, size } = renderTable(spec);
|
|
64
|
+
const body = nodes.map((node) => nodeMarkup(node.props)).join("");
|
|
65
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size.width} ${size.height}" width="${size.width}" height="${size.height}"><g font-family="sans-serif">${body}</g></svg>`;
|
|
66
|
+
}
|