@hwp-editor/react 1.0.0-rc.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/dist/index.js ADDED
@@ -0,0 +1,1737 @@
1
+ // src/HwpEditor.tsx
2
+ import {
3
+ forwardRef,
4
+ useCallback,
5
+ useEffect as useEffect3,
6
+ useImperativeHandle,
7
+ useMemo as useMemo3,
8
+ useRef,
9
+ useState as useState5,
10
+ useSyncExternalStore
11
+ } from "react";
12
+ import { createStore, segmentAtRef as segmentAtRef4 } from "@hwp-editor/core";
13
+
14
+ // src/context.ts
15
+ import { createContext, useContext } from "react";
16
+ var HwpEditorContext = createContext(
17
+ null
18
+ );
19
+ function useHwpEditorContext() {
20
+ const value = useContext(HwpEditorContext);
21
+ if (value === null) {
22
+ throw new Error("hwp-editor panels must be rendered inside <HwpEditor>");
23
+ }
24
+ return value;
25
+ }
26
+
27
+ // src/PageCanvas.tsx
28
+ import { useMemo } from "react";
29
+ import { base64, segmentAtRef, segmentRef, segmentText } from "@hwp-editor/core";
30
+
31
+ // src/geometry.ts
32
+ import { segmentAtOffset } from "@hwp-editor/core";
33
+ function clamp(value, min, max) {
34
+ return Math.min(max, Math.max(min, value));
35
+ }
36
+ function nearestSegment(envelope, pageIndex, yFraction, pageCount) {
37
+ if (envelope.segments.length === 0) return void 0;
38
+ const offset = clickOffset(envelope, pageIndex, yFraction, pageCount);
39
+ const hit = segmentAtOffset(envelope, offset);
40
+ if (hit !== void 0) return hit;
41
+ let best;
42
+ let bestDistance = Number.POSITIVE_INFINITY;
43
+ for (const segment of envelope.segments) {
44
+ const distance = Math.abs(segment.start - offset);
45
+ if (distance < bestDistance) {
46
+ best = segment;
47
+ bestDistance = distance;
48
+ }
49
+ }
50
+ return best;
51
+ }
52
+ function clickOffset(envelope, pageIndex, yFraction, pageCount) {
53
+ const total = Math.max(pageCount, 1);
54
+ const fraction = clamp(
55
+ (clamp(pageIndex, 0, total - 1) + clamp(yFraction, 0, 1)) / total,
56
+ 0,
57
+ 1
58
+ );
59
+ return Math.round(fraction * envelope.markdown.length);
60
+ }
61
+ function segmentBand(envelope, segment, pageCount) {
62
+ const length = envelope.markdown.length;
63
+ if (length === 0) return null;
64
+ const total = Math.max(pageCount, 1);
65
+ const center = (segment.start + segment.end) / 2;
66
+ const position = center / length * total;
67
+ const pageIndex = clamp(Math.floor(position), 0, total - 1);
68
+ const height = clamp((segment.end - segment.start) / length * total * 100, 2, 100);
69
+ const top = clamp((position - pageIndex) * 100 - height / 2, 0, 100 - height);
70
+ return { pageIndex, top, height };
71
+ }
72
+
73
+ // src/sanitize.ts
74
+ function sanitizeSvg(svgText) {
75
+ const doc = new DOMParser().parseFromString(svgText, "image/svg+xml");
76
+ const root = doc.documentElement;
77
+ if (root === null || root.nodeName === "parsererror" || doc.getElementsByTagName("parsererror").length > 0) {
78
+ return "";
79
+ }
80
+ const dangerous = root.querySelectorAll("script, foreignObject");
81
+ for (const node of Array.from(dangerous)) node.remove();
82
+ scrubElement(root);
83
+ return root.outerHTML;
84
+ }
85
+ function scrubElement(el) {
86
+ for (const attr of Array.from(el.attributes)) {
87
+ const name = attr.name.toLowerCase();
88
+ const value = attr.value.trim().toLowerCase();
89
+ if (name.startsWith("on")) {
90
+ el.removeAttribute(attr.name);
91
+ } else if ((name === "href" || name === "xlink:href") && value.startsWith("javascript:")) {
92
+ el.removeAttribute(attr.name);
93
+ }
94
+ }
95
+ for (const child of Array.from(el.children)) scrubElement(child);
96
+ }
97
+
98
+ // src/text.ts
99
+ function plainSegmentText(markdownSlice) {
100
+ return markdownSlice.split("\n").map(stripLine).join("\n").trim();
101
+ }
102
+ function stripLine(line) {
103
+ let out = line.trimEnd();
104
+ out = out.replace(/^#{1,6}\s+/, "");
105
+ out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
106
+ out = out.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
107
+ out = out.replace(/(\*\*|__)(.*?)\1/g, "$2");
108
+ out = out.replace(/(\*|_)(.*?)\1/g, "$2");
109
+ out = out.replace(/`([^`]*)`/g, "$1");
110
+ return out;
111
+ }
112
+
113
+ // src/PageCanvas.tsx
114
+ import { jsx, jsxs } from "react/jsx-runtime";
115
+ function PageCanvas() {
116
+ const { state, store, envelope, t } = useHwpEditorContext();
117
+ const { pages, selection, pendingOps } = state;
118
+ const pendingSegments = useMemo(() => {
119
+ if (envelope === null) return /* @__PURE__ */ new Set();
120
+ const keys = /* @__PURE__ */ new Set();
121
+ for (const op of pendingOps) {
122
+ const target = opSearchText(op);
123
+ if (target === null || target === "") continue;
124
+ for (const segment of envelope.segments) {
125
+ const plain = plainSegmentText(segmentText(envelope, segment));
126
+ if (plain.includes(target)) {
127
+ keys.add(`${segment.section}:${segment.para}`);
128
+ }
129
+ }
130
+ }
131
+ return keys;
132
+ }, [envelope, pendingOps]);
133
+ const selectedSegment = envelope !== null && selection !== null ? segmentAtRef(envelope, selection) : void 0;
134
+ if (pages.length === 0) {
135
+ return /* @__PURE__ */ jsx("div", { className: "hwped-canvas", role: "main", "aria-label": t("canvas.aria"), children: /* @__PURE__ */ jsx("div", { className: "hwped-empty", children: t("canvas.emptyPages") }) });
136
+ }
137
+ const selectAt = (pageIndex, yFraction) => {
138
+ if (envelope === null) return;
139
+ const segment = nearestSegment(envelope, pageIndex, yFraction, pages.length);
140
+ if (segment !== void 0) {
141
+ store.dispatch({ type: "select", selection: segmentRef(segment) });
142
+ }
143
+ };
144
+ return /* @__PURE__ */ jsx("div", { className: "hwped-canvas", role: "main", "aria-label": t("canvas.aria"), children: pages.map((page, pageIndex) => /* @__PURE__ */ jsx(
145
+ PageView,
146
+ {
147
+ page,
148
+ onClick: (e) => {
149
+ const rect = e.currentTarget.getBoundingClientRect();
150
+ selectAt(
151
+ pageIndex,
152
+ rect.height > 0 ? (e.clientY - rect.top) / rect.height : 0.5
153
+ );
154
+ },
155
+ onActivate: () => selectAt(pageIndex, 0.5),
156
+ bands: bandsForPage(
157
+ envelope,
158
+ pageIndex,
159
+ pages.length,
160
+ selectedSegment,
161
+ pendingSegments
162
+ )
163
+ },
164
+ page.page
165
+ )) });
166
+ }
167
+ function bandsForPage(envelope, pageIndex, pageCount, selected, pendingKeys) {
168
+ if (envelope === null) return [];
169
+ const bands = [];
170
+ for (const segment of envelope.segments) {
171
+ const key = `${segment.section}:${segment.para}`;
172
+ const isSelected = selected === segment;
173
+ const isPending = !isSelected && pendingKeys.has(key);
174
+ if (!isSelected && !isPending) continue;
175
+ const band = segmentBand(envelope, segment, pageCount);
176
+ if (band === null || band.pageIndex !== pageIndex) continue;
177
+ bands.push({
178
+ key: `${isSelected ? "sel" : "pend"}-${key}`,
179
+ className: isSelected ? "hwped-band hwped-band-selected" : "hwped-band hwped-band-pending",
180
+ top: band.top,
181
+ height: band.height
182
+ });
183
+ }
184
+ return bands;
185
+ }
186
+ function PageView(props) {
187
+ const { page, onClick, onActivate, bands } = props;
188
+ const { t } = useHwpEditorContext();
189
+ const label = t("page.label", { page: page.page });
190
+ const svgHtml = useMemo(
191
+ () => page.format === "svg" ? sanitizeSvg(new TextDecoder().decode(page.data)) : null,
192
+ [page]
193
+ );
194
+ const imgSrc = useMemo(() => {
195
+ if (page.format === "svg") return null;
196
+ const mime = page.format === "jpeg" ? "image/jpeg" : `image/${page.format}`;
197
+ return `data:${mime};base64,${base64.encode(page.data)}`;
198
+ }, [page]);
199
+ return /* @__PURE__ */ jsxs(
200
+ "div",
201
+ {
202
+ className: "hwped-page",
203
+ role: "button",
204
+ tabIndex: 0,
205
+ "aria-label": label,
206
+ style: { aspectRatio: `${page.width} / ${page.height}` },
207
+ onClick,
208
+ onKeyDown: (e) => {
209
+ if (e.key === "Enter" || e.key === " ") {
210
+ e.preventDefault();
211
+ onActivate();
212
+ }
213
+ },
214
+ children: [
215
+ svgHtml !== null ? /* @__PURE__ */ jsx(
216
+ "div",
217
+ {
218
+ className: "hwped-page-svg",
219
+ dangerouslySetInnerHTML: { __html: svgHtml }
220
+ }
221
+ ) : /* @__PURE__ */ jsx(
222
+ "img",
223
+ {
224
+ className: "hwped-page-img",
225
+ src: imgSrc ?? "",
226
+ alt: label,
227
+ width: page.width,
228
+ height: page.height
229
+ }
230
+ ),
231
+ bands.map((band) => /* @__PURE__ */ jsx(
232
+ "div",
233
+ {
234
+ className: band.className,
235
+ style: { top: `${band.top}%`, height: `${band.height}%` }
236
+ },
237
+ band.key
238
+ ))
239
+ ]
240
+ }
241
+ );
242
+ }
243
+ function opSearchText(op) {
244
+ if ("find" in op && typeof op.find === "string") return op.find;
245
+ if ("anchor" in op && typeof op.anchor === "string") return op.anchor;
246
+ if (op.kind === "delete-para" && typeof op.text === "string") return op.text;
247
+ return null;
248
+ }
249
+
250
+ // src/SegmentInspector.tsx
251
+ import { useEffect, useState } from "react";
252
+ import { segmentAtRef as segmentAtRef2, segmentText as segmentText2 } from "@hwp-editor/core";
253
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
254
+ var ALIGNMENTS = [
255
+ { value: "left", key: "segment.alignLeft" },
256
+ { value: "center", key: "segment.alignCenter" },
257
+ { value: "right", key: "segment.alignRight" },
258
+ { value: "justify", key: "segment.alignJustify" }
259
+ ];
260
+ function SegmentInspector() {
261
+ const { state, store, envelope, editable, t } = useHwpEditorContext();
262
+ const { selection } = state;
263
+ const segment = envelope !== null && selection !== null ? segmentAtRef2(envelope, selection) : void 0;
264
+ const currentText = envelope !== null && segment !== void 0 ? plainSegmentText(segmentText2(envelope, segment)) : "";
265
+ const [replaceWith, setReplaceWith] = useState("");
266
+ const [insertText, setInsertText] = useState("");
267
+ const [bold, setBold] = useState(false);
268
+ const [fontSize, setFontSize] = useState("");
269
+ const [color, setColor] = useState("");
270
+ useEffect(() => {
271
+ setReplaceWith("");
272
+ setInsertText("");
273
+ setBold(false);
274
+ setFontSize("");
275
+ setColor("");
276
+ }, [selection]);
277
+ if (segment === void 0 || selection === null) {
278
+ return /* @__PURE__ */ jsx2(
279
+ "div",
280
+ {
281
+ className: "hwped-panel",
282
+ role: "region",
283
+ "aria-label": t("segment.panelAria"),
284
+ children: /* @__PURE__ */ jsx2("p", { className: "hwped-hint", children: t("segment.hint") })
285
+ }
286
+ );
287
+ }
288
+ const find = currentText;
289
+ const disabled = !editable;
290
+ return /* @__PURE__ */ jsxs2(
291
+ "div",
292
+ {
293
+ className: "hwped-panel",
294
+ role: "region",
295
+ "aria-label": t("segment.panelAria"),
296
+ children: [
297
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-field", children: [
298
+ /* @__PURE__ */ jsx2("span", { className: "hwped-label", children: t("segment.selectedPara", {
299
+ section: selection.section,
300
+ para: selection.para
301
+ }) }),
302
+ /* @__PURE__ */ jsx2("div", { className: "hwped-quote", children: currentText || t("segment.emptyPara") })
303
+ ] }),
304
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-field", children: [
305
+ /* @__PURE__ */ jsx2("label", { className: "hwped-label", htmlFor: "hwped-replace", children: t("segment.replaceLabel") }),
306
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-row", children: [
307
+ /* @__PURE__ */ jsx2(
308
+ "input",
309
+ {
310
+ id: "hwped-replace",
311
+ className: "hwped-input",
312
+ type: "text",
313
+ value: replaceWith,
314
+ disabled,
315
+ onChange: (e) => setReplaceWith(e.target.value),
316
+ placeholder: t("segment.replacePlaceholder")
317
+ }
318
+ ),
319
+ /* @__PURE__ */ jsx2(
320
+ "button",
321
+ {
322
+ type: "button",
323
+ className: "hwped-btn",
324
+ disabled: disabled || replaceWith === "" || find === "",
325
+ onClick: () => store.dispatch({
326
+ type: "queueOp",
327
+ op: { kind: "replace", find, replace: replaceWith }
328
+ }),
329
+ children: t("segment.replaceSubmit")
330
+ }
331
+ )
332
+ ] })
333
+ ] }),
334
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-field", children: [
335
+ /* @__PURE__ */ jsx2("label", { className: "hwped-label", htmlFor: "hwped-insert", children: t("segment.insertLabel") }),
336
+ /* @__PURE__ */ jsx2(
337
+ "textarea",
338
+ {
339
+ id: "hwped-insert",
340
+ className: "hwped-textarea",
341
+ value: insertText,
342
+ disabled,
343
+ onChange: (e) => setInsertText(e.target.value),
344
+ placeholder: t("segment.insertPlaceholder"),
345
+ rows: 2
346
+ }
347
+ ),
348
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-row", children: [
349
+ /* @__PURE__ */ jsx2(
350
+ "button",
351
+ {
352
+ type: "button",
353
+ className: "hwped-btn",
354
+ disabled: disabled || insertText === "" || find === "",
355
+ onClick: () => store.dispatch({
356
+ type: "queueOp",
357
+ op: { kind: "insert-para-before", anchor: find, text: insertText }
358
+ }),
359
+ children: t("segment.insertBefore")
360
+ }
361
+ ),
362
+ /* @__PURE__ */ jsx2(
363
+ "button",
364
+ {
365
+ type: "button",
366
+ className: "hwped-btn",
367
+ disabled: disabled || insertText === "" || find === "",
368
+ onClick: () => store.dispatch({
369
+ type: "queueOp",
370
+ op: { kind: "insert-para", anchor: find, text: insertText }
371
+ }),
372
+ children: t("segment.insertAfter")
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsx2(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ className: "hwped-btn hwped-btn-danger",
380
+ disabled: disabled || find === "",
381
+ onClick: () => store.dispatch({
382
+ type: "queueOp",
383
+ op: { kind: "delete-para", text: find }
384
+ }),
385
+ children: t("segment.deletePara")
386
+ }
387
+ )
388
+ ] })
389
+ ] }),
390
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-field", children: [
391
+ /* @__PURE__ */ jsx2("span", { className: "hwped-label", children: t("segment.alignLabel") }),
392
+ /* @__PURE__ */ jsx2(
393
+ "div",
394
+ {
395
+ className: "hwped-row",
396
+ role: "group",
397
+ "aria-label": t("segment.alignGroupAria"),
398
+ children: ALIGNMENTS.map((align) => /* @__PURE__ */ jsx2(
399
+ "button",
400
+ {
401
+ type: "button",
402
+ className: "hwped-btn",
403
+ disabled: disabled || find === "",
404
+ onClick: () => store.dispatch({
405
+ type: "queueOp",
406
+ op: { kind: "set-align", find, alignment: align.value }
407
+ }),
408
+ children: t(align.key)
409
+ },
410
+ align.value
411
+ ))
412
+ }
413
+ )
414
+ ] }),
415
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-field", children: [
416
+ /* @__PURE__ */ jsx2("span", { className: "hwped-label", children: t("segment.formatLabel") }),
417
+ /* @__PURE__ */ jsxs2("div", { className: "hwped-row", children: [
418
+ /* @__PURE__ */ jsxs2("label", { className: "hwped-check", children: [
419
+ /* @__PURE__ */ jsx2(
420
+ "input",
421
+ {
422
+ type: "checkbox",
423
+ checked: bold,
424
+ disabled,
425
+ onChange: (e) => setBold(e.target.checked)
426
+ }
427
+ ),
428
+ t("segment.bold")
429
+ ] }),
430
+ /* @__PURE__ */ jsx2(
431
+ "input",
432
+ {
433
+ className: "hwped-input hwped-input-narrow",
434
+ type: "number",
435
+ min: 1,
436
+ value: fontSize,
437
+ disabled,
438
+ onChange: (e) => setFontSize(e.target.value),
439
+ placeholder: t("segment.sizePlaceholder"),
440
+ "aria-label": t("segment.sizeAria")
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsx2(
444
+ "input",
445
+ {
446
+ className: "hwped-input hwped-input-narrow",
447
+ type: "text",
448
+ value: color,
449
+ disabled,
450
+ onChange: (e) => setColor(e.target.value),
451
+ placeholder: "#FF0000",
452
+ "aria-label": t("segment.colorAria")
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx2(
456
+ "button",
457
+ {
458
+ type: "button",
459
+ className: "hwped-btn",
460
+ disabled: disabled || find === "" || !bold && fontSize === "" && color === "",
461
+ onClick: () => {
462
+ const props = {};
463
+ if (bold) props["bold"] = "on";
464
+ if (fontSize !== "") props["size"] = fontSize;
465
+ if (color !== "") props["color"] = color;
466
+ store.dispatch({
467
+ type: "queueOp",
468
+ op: { kind: "set-format", find, props }
469
+ });
470
+ },
471
+ children: t("segment.formatSubmit")
472
+ }
473
+ )
474
+ ] })
475
+ ] })
476
+ ]
477
+ }
478
+ );
479
+ }
480
+
481
+ // src/TableGrid.tsx
482
+ import { useEffect as useEffect2, useState as useState2 } from "react";
483
+
484
+ // src/tables.ts
485
+ import { segmentText as segmentText3 } from "@hwp-editor/core";
486
+ function isTableSlice(slice) {
487
+ return parseMarkdownTable(slice) !== null;
488
+ }
489
+ function parseMarkdownTable(slice) {
490
+ const lines = slice.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|") && line.endsWith("|"));
491
+ if (lines.length < 2) return null;
492
+ const rows = [];
493
+ let sawSeparator = false;
494
+ for (const line of lines) {
495
+ const cells = line.slice(1, -1).split("|").map((cell) => cell.trim());
496
+ if (cells.every((cell) => /^:?-{2,}:?$/.test(cell))) {
497
+ sawSeparator = true;
498
+ continue;
499
+ }
500
+ rows.push(cells);
501
+ }
502
+ return sawSeparator && rows.length > 0 ? rows : null;
503
+ }
504
+ function findTables(envelope) {
505
+ const tables = [];
506
+ for (const segment of envelope.segments) {
507
+ const rows = parseMarkdownTable(segmentText3(envelope, segment));
508
+ if (rows !== null) {
509
+ tables.push({ tableIndex: tables.length, segment, rows });
510
+ }
511
+ }
512
+ return tables;
513
+ }
514
+ function tableAtRef(envelope, ref) {
515
+ return findTables(envelope).find(
516
+ (table) => table.segment.section === ref.section && table.segment.para === ref.para
517
+ );
518
+ }
519
+
520
+ // src/TableGrid.tsx
521
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
522
+ function TableGrid() {
523
+ const { state, store, envelope, editable, t } = useHwpEditorContext();
524
+ const { selection } = state;
525
+ const table = envelope !== null && selection !== null ? tableAtRef(envelope, selection) : void 0;
526
+ const [cell, setCell] = useState2(null);
527
+ const [mergeAnchor, setMergeAnchor] = useState2(null);
528
+ const [cellValue, setCellValue] = useState2("");
529
+ useEffect2(() => {
530
+ setCell(null);
531
+ setMergeAnchor(null);
532
+ setCellValue("");
533
+ }, [selection]);
534
+ if (table === void 0) {
535
+ return /* @__PURE__ */ jsx3(
536
+ "div",
537
+ {
538
+ className: "hwped-panel",
539
+ role: "region",
540
+ "aria-label": t("table.panelAria"),
541
+ children: /* @__PURE__ */ jsx3("p", { className: "hwped-hint", children: t("table.hint") })
542
+ }
543
+ );
544
+ }
545
+ const disabled = !editable;
546
+ const tableIndex = table.tableIndex;
547
+ const queue = store.dispatch.bind(store);
548
+ return /* @__PURE__ */ jsxs3("div", { className: "hwped-panel", role: "region", "aria-label": t("table.panelAria"), children: [
549
+ /* @__PURE__ */ jsxs3("div", { className: "hwped-field", children: [
550
+ /* @__PURE__ */ jsx3("span", { className: "hwped-label", children: t("table.tableLabel", { index: tableIndex }) }),
551
+ /* @__PURE__ */ jsx3("table", { className: "hwped-grid", children: /* @__PURE__ */ jsx3("tbody", { children: table.rows.map((row, rowIndex) => /* @__PURE__ */ jsx3("tr", { children: row.map((text, colIndex) => {
552
+ const active = cell !== null && cell.row === rowIndex && cell.col === colIndex;
553
+ const anchor = mergeAnchor !== null && mergeAnchor.row === rowIndex && mergeAnchor.col === colIndex;
554
+ return /* @__PURE__ */ jsx3("td", { children: /* @__PURE__ */ jsx3(
555
+ "button",
556
+ {
557
+ type: "button",
558
+ className: "hwped-cell" + (active ? " hwped-cell-selected" : "") + (anchor ? " hwped-cell-anchor" : ""),
559
+ onClick: () => {
560
+ setCell({ row: rowIndex, col: colIndex });
561
+ setCellValue(plainSegmentText(text));
562
+ },
563
+ children: plainSegmentText(text) || " "
564
+ }
565
+ ) }, colIndex);
566
+ }) }, rowIndex)) }) })
567
+ ] }),
568
+ cell !== null && /* @__PURE__ */ jsxs3("div", { className: "hwped-field", children: [
569
+ /* @__PURE__ */ jsx3("label", { className: "hwped-label", htmlFor: "hwped-cell", children: t("table.cellLabel", { row: cell.row, col: cell.col }) }),
570
+ /* @__PURE__ */ jsxs3("div", { className: "hwped-row", children: [
571
+ /* @__PURE__ */ jsx3(
572
+ "input",
573
+ {
574
+ id: "hwped-cell",
575
+ className: "hwped-input",
576
+ type: "text",
577
+ value: cellValue,
578
+ disabled,
579
+ onChange: (e) => setCellValue(e.target.value)
580
+ }
581
+ ),
582
+ /* @__PURE__ */ jsx3(
583
+ "button",
584
+ {
585
+ type: "button",
586
+ className: "hwped-btn",
587
+ disabled,
588
+ onClick: () => queue({
589
+ type: "queueOp",
590
+ op: {
591
+ kind: "set-cell",
592
+ table: tableIndex,
593
+ row: cell.row,
594
+ col: cell.col,
595
+ value: cellValue
596
+ }
597
+ }),
598
+ children: t("table.setCell")
599
+ }
600
+ )
601
+ ] }),
602
+ /* @__PURE__ */ jsxs3("div", { className: "hwped-row", children: [
603
+ /* @__PURE__ */ jsx3(
604
+ "button",
605
+ {
606
+ type: "button",
607
+ className: "hwped-btn",
608
+ disabled,
609
+ onClick: () => setMergeAnchor({ ...cell }),
610
+ children: t("table.mergeAnchor")
611
+ }
612
+ ),
613
+ /* @__PURE__ */ jsx3(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ className: "hwped-btn",
618
+ disabled: disabled || mergeAnchor === null || mergeAnchor.row === cell.row && mergeAnchor.col === cell.col,
619
+ onClick: () => {
620
+ if (mergeAnchor === null) return;
621
+ queue({
622
+ type: "queueOp",
623
+ op: {
624
+ kind: "merge-cells",
625
+ table: tableIndex,
626
+ r1: Math.min(mergeAnchor.row, cell.row),
627
+ c1: Math.min(mergeAnchor.col, cell.col),
628
+ r2: Math.max(mergeAnchor.row, cell.row),
629
+ c2: Math.max(mergeAnchor.col, cell.col)
630
+ }
631
+ });
632
+ setMergeAnchor(null);
633
+ },
634
+ children: t("table.mergeWithAnchor")
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsx3(
638
+ "button",
639
+ {
640
+ type: "button",
641
+ className: "hwped-btn",
642
+ disabled,
643
+ onClick: () => queue({
644
+ type: "queueOp",
645
+ op: {
646
+ kind: "split-cell",
647
+ table: tableIndex,
648
+ row: cell.row,
649
+ col: cell.col
650
+ }
651
+ }),
652
+ children: t("table.splitCell")
653
+ }
654
+ )
655
+ ] })
656
+ ] }),
657
+ /* @__PURE__ */ jsxs3("div", { className: "hwped-field", children: [
658
+ /* @__PURE__ */ jsx3("span", { className: "hwped-label", children: t("table.rowsCols") }),
659
+ /* @__PURE__ */ jsxs3("div", { className: "hwped-row", children: [
660
+ /* @__PURE__ */ jsx3(
661
+ "button",
662
+ {
663
+ type: "button",
664
+ className: "hwped-btn",
665
+ disabled,
666
+ onClick: () => queue({
667
+ type: "queueOp",
668
+ op: cell === null ? { kind: "add-row", table: tableIndex } : { kind: "add-row", table: tableIndex, at: cell.row + 1 }
669
+ }),
670
+ children: t("table.addRow")
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx3(
674
+ "button",
675
+ {
676
+ type: "button",
677
+ className: "hwped-btn",
678
+ disabled,
679
+ onClick: () => queue({
680
+ type: "queueOp",
681
+ op: cell === null ? { kind: "add-col", table: tableIndex } : { kind: "add-col", table: tableIndex, at: cell.col + 1 }
682
+ }),
683
+ children: t("table.addCol")
684
+ }
685
+ ),
686
+ /* @__PURE__ */ jsx3(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ className: "hwped-btn hwped-btn-danger",
691
+ disabled: disabled || cell === null,
692
+ onClick: () => {
693
+ if (cell === null) return;
694
+ queue({
695
+ type: "queueOp",
696
+ op: { kind: "delete-row", table: tableIndex, row: cell.row }
697
+ });
698
+ },
699
+ children: t("table.deleteRow")
700
+ }
701
+ ),
702
+ /* @__PURE__ */ jsx3(
703
+ "button",
704
+ {
705
+ type: "button",
706
+ className: "hwped-btn hwped-btn-danger",
707
+ disabled: disabled || cell === null,
708
+ onClick: () => {
709
+ if (cell === null) return;
710
+ queue({
711
+ type: "queueOp",
712
+ op: { kind: "delete-col", table: tableIndex, col: cell.col }
713
+ });
714
+ },
715
+ children: t("table.deleteCol")
716
+ }
717
+ )
718
+ ] })
719
+ ] }),
720
+ /* @__PURE__ */ jsx3("div", { className: "hwped-field", children: /* @__PURE__ */ jsx3(
721
+ "button",
722
+ {
723
+ type: "button",
724
+ className: "hwped-btn hwped-btn-danger",
725
+ disabled,
726
+ onClick: () => queue({
727
+ type: "queueOp",
728
+ op: { kind: "delete-table", target: tableIndex }
729
+ }),
730
+ children: t("table.deleteTable")
731
+ }
732
+ ) })
733
+ ] });
734
+ }
735
+
736
+ // src/FieldsPanel.tsx
737
+ import { useMemo as useMemo2, useState as useState3 } from "react";
738
+ import { segmentAtRef as segmentAtRef3, segmentText as segmentText4 } from "@hwp-editor/core";
739
+
740
+ // src/fields.ts
741
+ import { segmentAtOffset as segmentAtOffset2 } from "@hwp-editor/core";
742
+ var SLOT_PATTERN = /\{\{\s*([^{}]+?)\s*\}\}/g;
743
+ function extractFieldSlots(envelope) {
744
+ const slots = [];
745
+ const seen = /* @__PURE__ */ new Set();
746
+ for (const match of envelope.markdown.matchAll(SLOT_PATTERN)) {
747
+ const name = match[1] ?? "";
748
+ const offset = match.index ?? 0;
749
+ const segment = segmentAtOffset2(envelope, offset);
750
+ if (segment === void 0 || seen.has(name)) continue;
751
+ seen.add(name);
752
+ slots.push({
753
+ name,
754
+ segment: { section: segment.section, para: segment.para }
755
+ });
756
+ }
757
+ return slots;
758
+ }
759
+
760
+ // src/FieldsPanel.tsx
761
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
762
+ function FieldsPanel() {
763
+ const { store, envelope, editable, t } = useHwpEditorContext();
764
+ const [drafts, setDrafts] = useState3({});
765
+ const slots = useMemo2(
766
+ () => envelope === null ? [] : extractFieldSlots(envelope),
767
+ [envelope]
768
+ );
769
+ if (envelope === null || slots.length === 0) {
770
+ return /* @__PURE__ */ jsx4(
771
+ "div",
772
+ {
773
+ className: "hwped-panel",
774
+ role: "region",
775
+ "aria-label": t("fields.panelAria"),
776
+ children: /* @__PURE__ */ jsx4("p", { className: "hwped-hint", children: t("fields.hint") })
777
+ }
778
+ );
779
+ }
780
+ return /* @__PURE__ */ jsx4(
781
+ "div",
782
+ {
783
+ className: "hwped-panel",
784
+ role: "region",
785
+ "aria-label": t("fields.panelAria"),
786
+ children: /* @__PURE__ */ jsx4("ul", { className: "hwped-fields", children: slots.map((slot) => {
787
+ const segment = segmentAtRef3(envelope, slot.segment);
788
+ const snippet = segment === void 0 ? "" : segmentText4(envelope, segment).trim().slice(0, 40);
789
+ const draft = drafts[slot.name] ?? "";
790
+ return /* @__PURE__ */ jsxs4("li", { className: "hwped-field-item", children: [
791
+ /* @__PURE__ */ jsx4("div", { className: "hwped-row", children: /* @__PURE__ */ jsx4(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ className: "hwped-btn hwped-btn-link",
796
+ title: snippet,
797
+ onClick: () => store.dispatch({ type: "select", selection: slot.segment }),
798
+ children: slot.name
799
+ }
800
+ ) }),
801
+ /* @__PURE__ */ jsxs4("div", { className: "hwped-row", children: [
802
+ /* @__PURE__ */ jsx4(
803
+ "input",
804
+ {
805
+ className: "hwped-input",
806
+ type: "text",
807
+ value: draft,
808
+ disabled: !editable,
809
+ placeholder: t("fields.valuePlaceholder"),
810
+ "aria-label": t("fields.fieldValueAria", { name: slot.name }),
811
+ onChange: (e) => setDrafts((prev) => ({ ...prev, [slot.name]: e.target.value }))
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsx4(
815
+ "button",
816
+ {
817
+ type: "button",
818
+ className: "hwped-btn",
819
+ disabled: !editable || draft === "",
820
+ onClick: () => store.dispatch({
821
+ type: "queueOp",
822
+ op: { kind: "set-field", name: slot.name, value: draft }
823
+ }),
824
+ children: t("fields.setValue")
825
+ }
826
+ )
827
+ ] })
828
+ ] }, slot.name);
829
+ }) })
830
+ }
831
+ );
832
+ }
833
+
834
+ // src/ComposePanel.tsx
835
+ import { useState as useState4 } from "react";
836
+
837
+ // src/errors.ts
838
+ import { isHwpEngineError, isHwpErrorCode } from "@hwp-editor/core";
839
+ function toEditorError(e) {
840
+ return isHwpEngineError(e) && isHwpErrorCode(e.code) ? { code: e.code, message: e.message } : { message: e instanceof Error ? e.message : String(e) };
841
+ }
842
+ var KIND_BY_CODE = {
843
+ timeout: "timeout",
844
+ unavailable: "unavailable",
845
+ version: "unavailable",
846
+ protected: "protected",
847
+ failed: "generic",
848
+ bad_request: "generic",
849
+ unsupported_format: "generic",
850
+ output_too_large: "generic",
851
+ cancelled: "generic",
852
+ method_not_allowed: "generic",
853
+ not_found: "generic",
854
+ session_not_found: "generic",
855
+ path_traversal: "generic",
856
+ forbidden: "generic",
857
+ internal: "generic"
858
+ };
859
+ function engineErrorKind(error) {
860
+ if (error.code !== void 0 && Object.hasOwn(KIND_BY_CODE, error.code)) {
861
+ return KIND_BY_CODE[error.code];
862
+ }
863
+ return classifyEngineError(error.message);
864
+ }
865
+ function classifyEngineError(message) {
866
+ const m = message.toLowerCase();
867
+ if (m.includes("timed out") || m.includes("hwp_timeout") || m.includes("http 504")) {
868
+ return "timeout";
869
+ }
870
+ if (m.includes("binary not found") || m.includes("cli_missing") || m.includes("not executable") || m.includes("http 503")) {
871
+ return "unavailable";
872
+ }
873
+ if (m.includes("distribution") || m.includes("encrypted") || m.includes("protected") || m.includes("\uBC30\uD3EC\uC6A9")) {
874
+ return "protected";
875
+ }
876
+ return "generic";
877
+ }
878
+
879
+ // src/ErrorLine.tsx
880
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
881
+ var LABEL_KEY_BY_KIND = {
882
+ timeout: "error.kind.timeout",
883
+ unavailable: "error.kind.unavailable",
884
+ protected: "error.kind.protected",
885
+ generic: "error.kind.generic"
886
+ };
887
+ function ErrorLine(props) {
888
+ const { t } = useHwpEditorContext();
889
+ const kind = engineErrorKind({
890
+ // Conditional spread: exactOptionalPropertyTypes rejects an explicit
891
+ // `code: undefined` against the optional field.
892
+ ...props.code !== void 0 ? { code: props.code } : {},
893
+ message: props.message
894
+ });
895
+ return /* @__PURE__ */ jsxs5("p", { className: "hwped-error", role: "alert", "data-error-kind": kind, children: [
896
+ kind !== "generic" && /* @__PURE__ */ jsx5("span", { className: `hwped-error-kind hwped-error-${kind}`, children: t(LABEL_KEY_BY_KIND[kind]) }),
897
+ props.prefix,
898
+ ": ",
899
+ props.message
900
+ ] });
901
+ }
902
+
903
+ // src/presets.ts
904
+ var COMPOSE_PRESETS = [
905
+ "official",
906
+ "report",
907
+ "plan",
908
+ "notice",
909
+ "minutes",
910
+ "gaejosik",
911
+ "press"
912
+ ];
913
+ var MALGUN = "\uB9D1\uC740 \uACE0\uB515";
914
+ var HCR_BATANG = "\uD568\uCD08\uB86C\uBC14\uD0D5";
915
+ var PRESET_PROFILES = {
916
+ official: { fontFamily: MALGUN, bodySizePt: 12, lineHeightPercent: 160, pageNumber: false },
917
+ report: { fontFamily: HCR_BATANG, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
918
+ plan: { fontFamily: HCR_BATANG, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
919
+ notice: { fontFamily: MALGUN, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
920
+ minutes: { fontFamily: HCR_BATANG, bodySizePt: 14, lineHeightPercent: 130, pageNumber: false },
921
+ gaejosik: { fontFamily: MALGUN, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
922
+ press: { fontFamily: HCR_BATANG, bodySizePt: 14, lineHeightPercent: 160, pageNumber: true }
923
+ };
924
+ function bodyToBlocks(body) {
925
+ const blocks = [];
926
+ for (const chunk of body.split(/\n\s*\n/)) {
927
+ const text = chunk.trim();
928
+ if (text === "") continue;
929
+ if (text.startsWith("# ")) {
930
+ blocks.push({
931
+ type: "paragraph",
932
+ style: "heading",
933
+ runs: [{ type: "text", text: text.slice(2).trim() }]
934
+ });
935
+ } else {
936
+ blocks.push({
937
+ type: "paragraph",
938
+ style: "body",
939
+ runs: [{ type: "text", text }]
940
+ });
941
+ }
942
+ }
943
+ return blocks;
944
+ }
945
+ function buildDocumentSpec(preset, input) {
946
+ const profile = PRESET_PROFILES[preset];
947
+ const blocks = [];
948
+ if (input.title.trim() !== "") {
949
+ blocks.push({
950
+ type: "paragraph",
951
+ style: "title",
952
+ runs: [{ type: "text", text: input.title.trim() }]
953
+ });
954
+ }
955
+ blocks.push(...bodyToBlocks(input.body));
956
+ if (blocks.length === 0) {
957
+ blocks.push({
958
+ type: "paragraph",
959
+ style: "body",
960
+ runs: [{ type: "text", text: " " }]
961
+ });
962
+ }
963
+ const section = {
964
+ ...profile.pageNumber ? { page_number: { position: "bottom_center" } } : {},
965
+ blocks
966
+ };
967
+ return {
968
+ version: "2.0",
969
+ document: {
970
+ version: "1.0",
971
+ metadata: {
972
+ ...input.title.trim() !== "" ? { title: input.title.trim() } : {},
973
+ ...input.author.trim() !== "" ? { author: input.author.trim() } : {}
974
+ },
975
+ styles: {
976
+ body: {
977
+ font_family: profile.fontFamily,
978
+ font_size_pt: profile.bodySizePt,
979
+ line_height_percent: profile.lineHeightPercent,
980
+ align: "justify"
981
+ },
982
+ heading: {
983
+ font_family: profile.fontFamily,
984
+ font_size_pt: profile.bodySizePt + 3,
985
+ bold: true,
986
+ spacing_before_pt: 13,
987
+ spacing_after_pt: 6
988
+ },
989
+ title: {
990
+ font_family: profile.fontFamily,
991
+ font_size_pt: profile.bodySizePt + 6,
992
+ bold: true,
993
+ align: "center",
994
+ spacing_after_pt: 18
995
+ }
996
+ },
997
+ sections: [section]
998
+ }
999
+ };
1000
+ }
1001
+
1002
+ // src/ComposePanel.tsx
1003
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1004
+ var PRESET_LABEL_KEYS = {
1005
+ official: "presets.official",
1006
+ report: "presets.report",
1007
+ plan: "presets.plan",
1008
+ notice: "presets.notice",
1009
+ minutes: "presets.minutes",
1010
+ gaejosik: "presets.gaejosik",
1011
+ press: "presets.press"
1012
+ };
1013
+ function ComposePanel(props) {
1014
+ const { engine, t, onError } = useHwpEditorContext();
1015
+ const [preset, setPreset] = useState4("official");
1016
+ const [title, setTitle] = useState4("");
1017
+ const [author, setAuthor] = useState4("");
1018
+ const [body, setBody] = useState4("");
1019
+ const [busy, setBusy] = useState4(false);
1020
+ const [error, setError] = useState4(null);
1021
+ const compose = async () => {
1022
+ setBusy(true);
1023
+ setError(null);
1024
+ try {
1025
+ const spec = buildDocumentSpec(preset, { title, author, body });
1026
+ const stem = title.trim() === "" ? t("compose.defaultFileStem") : title.trim();
1027
+ const result = await engine.compose(spec, `${stem}.hwpx`);
1028
+ props.onComposed(result.document);
1029
+ } catch (e) {
1030
+ setError(toEditorError(e));
1031
+ onError?.(e);
1032
+ } finally {
1033
+ setBusy(false);
1034
+ }
1035
+ };
1036
+ return /* @__PURE__ */ jsx6("div", { className: "hwped-overlay", role: "presentation", onClick: props.onClose, children: /* @__PURE__ */ jsxs6(
1037
+ "div",
1038
+ {
1039
+ className: "hwped-dialog",
1040
+ role: "dialog",
1041
+ "aria-modal": "true",
1042
+ "aria-label": t("compose.title"),
1043
+ onClick: (e) => e.stopPropagation(),
1044
+ children: [
1045
+ /* @__PURE__ */ jsx6("h2", { className: "hwped-dialog-title", children: t("compose.title") }),
1046
+ /* @__PURE__ */ jsxs6("div", { className: "hwped-field", children: [
1047
+ /* @__PURE__ */ jsx6("span", { className: "hwped-label", children: t("compose.docType") }),
1048
+ /* @__PURE__ */ jsx6(
1049
+ "div",
1050
+ {
1051
+ className: "hwped-presets",
1052
+ role: "radiogroup",
1053
+ "aria-label": t("compose.docTypeAria"),
1054
+ children: COMPOSE_PRESETS.map((value) => /* @__PURE__ */ jsx6(
1055
+ "button",
1056
+ {
1057
+ type: "button",
1058
+ role: "radio",
1059
+ "aria-checked": preset === value,
1060
+ className: "hwped-preset" + (preset === value ? " hwped-preset-active" : ""),
1061
+ onClick: () => setPreset(value),
1062
+ children: t(PRESET_LABEL_KEYS[value])
1063
+ },
1064
+ value
1065
+ ))
1066
+ }
1067
+ )
1068
+ ] }),
1069
+ /* @__PURE__ */ jsxs6("div", { className: "hwped-field", children: [
1070
+ /* @__PURE__ */ jsx6("label", { className: "hwped-label", htmlFor: "hwped-compose-title", children: t("compose.titleLabel") }),
1071
+ /* @__PURE__ */ jsx6(
1072
+ "input",
1073
+ {
1074
+ id: "hwped-compose-title",
1075
+ className: "hwped-input",
1076
+ type: "text",
1077
+ value: title,
1078
+ onChange: (e) => setTitle(e.target.value),
1079
+ placeholder: t("compose.titlePlaceholder")
1080
+ }
1081
+ )
1082
+ ] }),
1083
+ /* @__PURE__ */ jsxs6("div", { className: "hwped-field", children: [
1084
+ /* @__PURE__ */ jsx6("label", { className: "hwped-label", htmlFor: "hwped-compose-author", children: t("compose.authorLabel") }),
1085
+ /* @__PURE__ */ jsx6(
1086
+ "input",
1087
+ {
1088
+ id: "hwped-compose-author",
1089
+ className: "hwped-input",
1090
+ type: "text",
1091
+ value: author,
1092
+ onChange: (e) => setAuthor(e.target.value),
1093
+ placeholder: t("compose.authorPlaceholder")
1094
+ }
1095
+ )
1096
+ ] }),
1097
+ /* @__PURE__ */ jsxs6("div", { className: "hwped-field", children: [
1098
+ /* @__PURE__ */ jsx6("label", { className: "hwped-label", htmlFor: "hwped-compose-body", children: t("compose.bodyLabel") }),
1099
+ /* @__PURE__ */ jsx6(
1100
+ "textarea",
1101
+ {
1102
+ id: "hwped-compose-body",
1103
+ className: "hwped-textarea",
1104
+ rows: 8,
1105
+ value: body,
1106
+ onChange: (e) => setBody(e.target.value),
1107
+ placeholder: t("compose.bodyPlaceholder")
1108
+ }
1109
+ )
1110
+ ] }),
1111
+ error !== null && /* @__PURE__ */ jsx6(ErrorLine, { prefix: t("error.prefix.compose"), ...error }),
1112
+ /* @__PURE__ */ jsxs6("div", { className: "hwped-row hwped-dialog-actions", children: [
1113
+ /* @__PURE__ */ jsx6(
1114
+ "button",
1115
+ {
1116
+ type: "button",
1117
+ className: "hwped-btn",
1118
+ onClick: props.onClose,
1119
+ disabled: busy,
1120
+ children: t("compose.cancel")
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ jsx6(
1124
+ "button",
1125
+ {
1126
+ type: "button",
1127
+ className: "hwped-btn hwped-btn-primary",
1128
+ onClick: () => void compose(),
1129
+ disabled: busy,
1130
+ children: busy ? t("compose.submitting") : t("compose.submit")
1131
+ }
1132
+ )
1133
+ ] })
1134
+ ]
1135
+ }
1136
+ ) });
1137
+ }
1138
+
1139
+ // src/messages.ts
1140
+ var en = {
1141
+ // Toolbar
1142
+ "toolbar.toolsAria": "Editing tools",
1143
+ "toolbar.readOnly": "Read-only",
1144
+ "toolbar.pendingEdits": (p) => p.count === 1 ? "1 pending edit" : `${p.count} pending edits`,
1145
+ "toolbar.revert": "Undo edit",
1146
+ "toolbar.apply": "Apply",
1147
+ "toolbar.applyWithCount": (p) => `Apply (${p.count})`,
1148
+ "toolbar.applying": "Applying\u2026",
1149
+ "toolbar.newDocument": "New document",
1150
+ // Validation badge
1151
+ "validation.aria": "Validation result",
1152
+ "validation.valid": "Valid",
1153
+ "validation.errors": (p) => p.count === 1 ? "1 error" : `${p.count} errors`,
1154
+ // Canvas
1155
+ "canvas.aria": "Document pages",
1156
+ "canvas.empty": "No document open.",
1157
+ "canvas.createCta": "Create a new document",
1158
+ "canvas.emptyPages": "No rendered pages.",
1159
+ "page.label": (p) => `Page ${p.page}`,
1160
+ // Side panel shell
1161
+ "side.panelAria": "Editing panels",
1162
+ "tabs.para": "Paragraph",
1163
+ "tabs.table": "Table",
1164
+ "tabs.fields": "Fields",
1165
+ // Error line — the prefix names which operation failed. The `{message}`
1166
+ // tail stays engine-authored and is never translated.
1167
+ "error.prefix.load": "Failed to open document",
1168
+ "error.prefix.apply": "Failed to apply edits",
1169
+ "error.prefix.compose": "Failed to create document",
1170
+ // Badge labels; "generic" renders no chip, hence the empty string.
1171
+ "error.kind.timeout": "Engine timed out",
1172
+ "error.kind.unavailable": "hwp binary unavailable",
1173
+ "error.kind.protected": "Protected document",
1174
+ "error.kind.generic": "",
1175
+ // Compose dialog
1176
+ "compose.title": "Create a new document",
1177
+ "compose.docType": "Document type",
1178
+ "compose.docTypeAria": "Document type",
1179
+ "compose.titleLabel": "Title",
1180
+ "compose.titlePlaceholder": "Document title",
1181
+ "compose.authorLabel": "Author",
1182
+ "compose.authorPlaceholder": "Author (optional)",
1183
+ "compose.bodyLabel": "Body",
1184
+ "compose.bodyPlaceholder": "Blank lines separate paragraphs. Lines starting with '# ' become subheadings.\n\nExample:\n# 1. Overview\nBody text...",
1185
+ "compose.cancel": "Cancel",
1186
+ "compose.submit": "Create document",
1187
+ "compose.submitting": "Creating\u2026",
1188
+ /** Filename stem used when the compose form has no title. */
1189
+ "compose.defaultFileStem": "New document",
1190
+ // Compose preset labels. The preset PROFILES (font names, sizes) are
1191
+ // document data and stay in presets.ts, untranslated.
1192
+ "presets.official": "Official document",
1193
+ "presets.report": "Report",
1194
+ "presets.plan": "Plan",
1195
+ "presets.notice": "Notice",
1196
+ "presets.minutes": "Minutes",
1197
+ "presets.gaejosik": "Outline document",
1198
+ "presets.press": "Press release",
1199
+ // SegmentInspector
1200
+ "segment.panelAria": "Paragraph editor",
1201
+ "segment.hint": "Click the page to select a paragraph to edit.",
1202
+ "segment.selectedPara": (p) => `Selected paragraph (section ${p.section}, paragraph ${p.para})`,
1203
+ "segment.emptyPara": "(empty paragraph)",
1204
+ "segment.replaceLabel": "Replace text",
1205
+ "segment.replacePlaceholder": "New text",
1206
+ "segment.replaceSubmit": "Replace",
1207
+ "segment.insertLabel": "Insert paragraph",
1208
+ "segment.insertPlaceholder": "Paragraph text to insert",
1209
+ "segment.insertBefore": "Insert before",
1210
+ "segment.insertAfter": "Insert after",
1211
+ "segment.deletePara": "Delete paragraph",
1212
+ "segment.alignLabel": "Alignment",
1213
+ "segment.alignGroupAria": "Paragraph alignment",
1214
+ "segment.alignLeft": "Left",
1215
+ "segment.alignCenter": "Center",
1216
+ "segment.alignRight": "Right",
1217
+ "segment.alignJustify": "Justify",
1218
+ "segment.formatLabel": "Character format",
1219
+ "segment.bold": "Bold",
1220
+ "segment.sizePlaceholder": "Size (pt)",
1221
+ "segment.sizeAria": "Font size (pt)",
1222
+ "segment.colorAria": "Font color",
1223
+ "segment.formatSubmit": "Apply format",
1224
+ // TableGrid
1225
+ "table.panelAria": "Table editor",
1226
+ "table.hint": "Select a region containing a table on the page. The current selection is not a table paragraph.",
1227
+ "table.tableLabel": (p) => `Table #${p.index}`,
1228
+ "table.cellLabel": (p) => `Cell (${p.row}, ${p.col})`,
1229
+ "table.setCell": "Set cell",
1230
+ "table.mergeAnchor": "Set merge anchor",
1231
+ "table.mergeWithAnchor": "Merge with anchor",
1232
+ "table.splitCell": "Split cell",
1233
+ "table.rowsCols": "Rows / columns",
1234
+ "table.addRow": "Add row",
1235
+ "table.addCol": "Add column",
1236
+ "table.deleteRow": "Delete row",
1237
+ "table.deleteCol": "Delete column",
1238
+ "table.deleteTable": "Delete table",
1239
+ // FieldsPanel
1240
+ "fields.panelAria": "Fields",
1241
+ "fields.hint": "No {{name}} field placeholders were found in the document.",
1242
+ "fields.valuePlaceholder": "New value",
1243
+ "fields.fieldValueAria": (p) => `Value for field ${p.name}`,
1244
+ "fields.setValue": "Set value"
1245
+ };
1246
+ var ko = {
1247
+ "toolbar.toolsAria": "\uD3B8\uC9D1 \uB3C4\uAD6C",
1248
+ "toolbar.readOnly": "\uC77D\uAE30 \uC804\uC6A9",
1249
+ "toolbar.pendingEdits": (p) => `\uB300\uAE30 \uD3B8\uC9D1 ${p.count}`,
1250
+ "toolbar.revert": "\uB418\uB3CC\uB9AC\uAE30",
1251
+ "toolbar.apply": "\uC801\uC6A9",
1252
+ "toolbar.applyWithCount": (p) => `\uC801\uC6A9 (${p.count})`,
1253
+ "toolbar.applying": "\uC801\uC6A9 \uC911...",
1254
+ "toolbar.newDocument": "\uC0C8 \uBB38\uC11C",
1255
+ "validation.aria": "\uAC80\uC99D \uACB0\uACFC",
1256
+ "validation.valid": "\uC720\uD6A8",
1257
+ "validation.errors": (p) => `\uC624\uB958 ${p.count}\uAC74`,
1258
+ "canvas.aria": "\uBB38\uC11C \uD398\uC774\uC9C0",
1259
+ "canvas.empty": "\uC5F4\uB9B0 \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
1260
+ "canvas.createCta": "\uC0C8 \uBB38\uC11C \uB9CC\uB4E4\uAE30",
1261
+ "canvas.emptyPages": "\uB80C\uB354\uB9C1\uB41C \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
1262
+ "page.label": (p) => `\uD398\uC774\uC9C0 ${p.page}`,
1263
+ "side.panelAria": "\uD3B8\uC9D1 \uD328\uB110",
1264
+ "tabs.para": "\uBB38\uB2E8",
1265
+ "tabs.table": "\uD45C",
1266
+ "tabs.fields": "\uD544\uB4DC",
1267
+ "error.prefix.load": "\uBB38\uC11C \uC5F4\uAE30 \uC2E4\uD328",
1268
+ "error.prefix.apply": "\uD3B8\uC9D1 \uC801\uC6A9 \uC2E4\uD328",
1269
+ "error.prefix.compose": "\uBB38\uC11C \uC0DD\uC131 \uC2E4\uD328",
1270
+ "error.kind.timeout": "\uC5D4\uC9C4 \uC2DC\uAC04 \uCD08\uACFC",
1271
+ "error.kind.unavailable": "hwp \uC2E4\uD589 \uD30C\uC77C \uC5C6\uC74C",
1272
+ "error.kind.protected": "\uBCF4\uD638/\uBC30\uD3EC \uBB38\uC11C",
1273
+ "error.kind.generic": "",
1274
+ "compose.title": "\uC0C8 \uBB38\uC11C \uB9CC\uB4E4\uAE30",
1275
+ "compose.docType": "\uBB38\uC11C \uC720\uD615",
1276
+ "compose.docTypeAria": "\uBB38\uC11C \uC720\uD615",
1277
+ "compose.titleLabel": "\uC81C\uBAA9",
1278
+ "compose.titlePlaceholder": "\uBB38\uC11C \uC81C\uBAA9",
1279
+ "compose.authorLabel": "\uC791\uC131\uC790",
1280
+ "compose.authorPlaceholder": "\uC791\uC131\uC790 (\uC120\uD0DD)",
1281
+ "compose.bodyLabel": "\uBCF8\uBB38",
1282
+ "compose.bodyPlaceholder": "\uBE48 \uC904\uB85C \uBB38\uB2E8\uC744 \uAD6C\uBD84\uD569\uB2C8\uB2E4. '# '\uB85C \uC2DC\uC791\uD558\uB294 \uC904\uC740 \uC18C\uC81C\uBAA9\uC774 \uB429\uB2C8\uB2E4.\n\n\uC608)\n# 1. \uAC1C\uC694\n\uBCF8\uBB38 \uB0B4\uC6A9...",
1283
+ "compose.cancel": "\uCDE8\uC18C",
1284
+ "compose.submit": "\uBB38\uC11C \uC0DD\uC131",
1285
+ "compose.submitting": "\uC0DD\uC131 \uC911...",
1286
+ "compose.defaultFileStem": "\uC0C8 \uBB38\uC11C",
1287
+ "presets.official": "\uACF5\uBB38",
1288
+ "presets.report": "\uBCF4\uACE0\uC11C",
1289
+ "presets.plan": "\uACC4\uD68D\uC11C",
1290
+ "presets.notice": "\uC548\uB0B4\uBB38",
1291
+ "presets.minutes": "\uD68C\uC758\uB85D",
1292
+ "presets.gaejosik": "\uAC1C\uC870\uC2DD \uBB38\uC11C",
1293
+ "presets.press": "\uBCF4\uB3C4\uC790\uB8CC",
1294
+ "segment.panelAria": "\uBB38\uB2E8 \uD3B8\uC9D1",
1295
+ "segment.hint": "\uD398\uC774\uC9C0\uB97C \uD074\uB9AD\uD574 \uD3B8\uC9D1\uD560 \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
1296
+ "segment.selectedPara": (p) => `\uC120\uD0DD \uBB38\uB2E8 (\uAD6C\uC5ED ${p.section}, \uBB38\uB2E8 ${p.para})`,
1297
+ "segment.emptyPara": "(\uBE48 \uBB38\uB2E8)",
1298
+ "segment.replaceLabel": "\uD14D\uC2A4\uD2B8 \uAD50\uCCB4",
1299
+ "segment.replacePlaceholder": "\uC0C8 \uD14D\uC2A4\uD2B8",
1300
+ "segment.replaceSubmit": "\uAD50\uCCB4",
1301
+ "segment.insertLabel": "\uBB38\uB2E8 \uC0BD\uC785",
1302
+ "segment.insertPlaceholder": "\uC0BD\uC785\uD560 \uBB38\uB2E8 \uD14D\uC2A4\uD2B8",
1303
+ "segment.insertBefore": "\uC55E\uC5D0 \uC0BD\uC785",
1304
+ "segment.insertAfter": "\uB4A4\uC5D0 \uC0BD\uC785",
1305
+ "segment.deletePara": "\uBB38\uB2E8 \uC0AD\uC81C",
1306
+ "segment.alignLabel": "\uC815\uB82C",
1307
+ "segment.alignGroupAria": "\uBB38\uB2E8 \uC815\uB82C",
1308
+ "segment.alignLeft": "\uC67C\uCABD",
1309
+ "segment.alignCenter": "\uAC00\uC6B4\uB370",
1310
+ "segment.alignRight": "\uC624\uB978\uCABD",
1311
+ "segment.alignJustify": "\uC591\uCABD",
1312
+ "segment.formatLabel": "\uAE00\uC790 \uC11C\uC2DD",
1313
+ "segment.bold": "\uAD75\uAC8C",
1314
+ "segment.sizePlaceholder": "\uD06C\uAE30(pt)",
1315
+ "segment.sizeAria": "\uAE00\uC790 \uD06C\uAE30(pt)",
1316
+ "segment.colorAria": "\uAE00\uC790 \uC0C9\uC0C1",
1317
+ "segment.formatSubmit": "\uC11C\uC2DD \uC801\uC6A9",
1318
+ "table.panelAria": "\uD45C \uD3B8\uC9D1",
1319
+ "table.hint": "\uD45C\uAC00 \uD3EC\uD568\uB41C \uC601\uC5ED\uC744 \uD398\uC774\uC9C0\uC5D0\uC11C \uC120\uD0DD\uD558\uC138\uC694. \uD45C\uAC00 \uC544\uB2CC \uBB38\uB2E8\uC774 \uC120\uD0DD\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
1320
+ "table.tableLabel": (p) => `\uD45C #${p.index}`,
1321
+ "table.cellLabel": (p) => `\uC140 (${p.row}, ${p.col})`,
1322
+ "table.setCell": "\uC140 \uC124\uC815",
1323
+ "table.mergeAnchor": "\uBCD1\uD569 \uAE30\uC900 \uC9C0\uC815",
1324
+ "table.mergeWithAnchor": "\uAE30\uC900 \uC140\uACFC \uBCD1\uD569",
1325
+ "table.splitCell": "\uC140 \uBD84\uD560",
1326
+ "table.rowsCols": "\uD589/\uC5F4",
1327
+ "table.addRow": "\uD589 \uCD94\uAC00",
1328
+ "table.addCol": "\uC5F4 \uCD94\uAC00",
1329
+ "table.deleteRow": "\uD589 \uC0AD\uC81C",
1330
+ "table.deleteCol": "\uC5F4 \uC0AD\uC81C",
1331
+ "table.deleteTable": "\uD45C \uC0AD\uC81C",
1332
+ "fields.panelAria": "\uD544\uB4DC",
1333
+ "fields.hint": "\uBB38\uC11C\uC5D0\uC11C {{\uC774\uB984}} \uD615\uD0DC\uC758 \uD544\uB4DC \uC790\uB9AC\uD45C\uC2DC\uC790\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
1334
+ "fields.valuePlaceholder": "\uC0C8 \uAC12",
1335
+ "fields.fieldValueAria": (p) => `\uD544\uB4DC ${p.name} \uAC12`,
1336
+ "fields.setValue": "\uC124\uC815"
1337
+ };
1338
+ function createT(locale, messages) {
1339
+ const table = {
1340
+ ...en,
1341
+ ...locale === "ko" ? ko : {},
1342
+ ...messages
1343
+ };
1344
+ return ((key, params) => {
1345
+ const value = table[key];
1346
+ return typeof value === "function" ? value(params) : value;
1347
+ });
1348
+ }
1349
+
1350
+ // src/HwpEditor.tsx
1351
+ import { segmentText as segmentText5 } from "@hwp-editor/core";
1352
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1353
+ var RENDER_SVG = { format: "svg" };
1354
+ var HwpEditor = forwardRef(
1355
+ function HwpEditor2(props, ref) {
1356
+ const {
1357
+ engine,
1358
+ file,
1359
+ locale = "en",
1360
+ messages,
1361
+ readOnly = false,
1362
+ onReady,
1363
+ onError,
1364
+ onChange,
1365
+ onDirtyChange,
1366
+ className,
1367
+ style
1368
+ } = props;
1369
+ const cbRef = useRef({ onReady, onError, onChange, onDirtyChange });
1370
+ useEffect3(() => {
1371
+ cbRef.current = { onReady, onError, onChange, onDirtyChange };
1372
+ });
1373
+ const emitReady = useCallback((document) => {
1374
+ cbRef.current.onReady?.(document);
1375
+ }, []);
1376
+ const emitError = useCallback((error) => {
1377
+ cbRef.current.onError?.(error);
1378
+ }, []);
1379
+ const emitChange = useCallback((document) => {
1380
+ cbRef.current.onChange?.(document);
1381
+ }, []);
1382
+ const emitDirty = useCallback((dirty2) => {
1383
+ cbRef.current.onDirtyChange?.(dirty2);
1384
+ }, []);
1385
+ const t = useMemo3(() => createT(locale, messages), [locale, messages]);
1386
+ const [store] = useState5(() => createStore());
1387
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
1388
+ const [envelope, setEnvelope] = useState5(null);
1389
+ const [capabilities, setCapabilities] = useState5(null);
1390
+ const [validation, setValidation] = useState5(null);
1391
+ const [loadError, setLoadError] = useState5(null);
1392
+ const [tab, setTab] = useState5("para");
1393
+ const [composing, setComposing] = useState5(false);
1394
+ useEffect3(() => {
1395
+ let cancelled = false;
1396
+ if (file === null) {
1397
+ setEnvelope(null);
1398
+ setValidation(null);
1399
+ setLoadError(null);
1400
+ return;
1401
+ }
1402
+ (async () => {
1403
+ try {
1404
+ const caps = await engine.capabilities();
1405
+ const [nextEnvelope, pages, report] = await Promise.all([
1406
+ engine.read(file),
1407
+ engine.render(file, RENDER_SVG),
1408
+ engine.validate(file).catch(() => null)
1409
+ ]);
1410
+ if (cancelled) return;
1411
+ setCapabilities(caps);
1412
+ setEnvelope(nextEnvelope);
1413
+ setValidation(report);
1414
+ setLoadError(null);
1415
+ store.dispatch({ type: "load", document: file, pages });
1416
+ emitReady(file);
1417
+ } catch (e) {
1418
+ if (cancelled) return;
1419
+ setLoadError(toEditorError(e));
1420
+ emitError(e);
1421
+ }
1422
+ })();
1423
+ return () => {
1424
+ cancelled = true;
1425
+ };
1426
+ }, [engine, file, store]);
1427
+ const editable = !readOnly && (capabilities?.editable ?? true);
1428
+ useEffect3(() => {
1429
+ if (envelope === null || state.selection === null) return;
1430
+ const segment = segmentAtRef4(envelope, state.selection);
1431
+ if (segment === void 0) return;
1432
+ setTab(
1433
+ isTableSlice(segmentText5(envelope, segment)) ? "table" : "para"
1434
+ );
1435
+ }, [envelope, state.selection]);
1436
+ const dirty = state.pendingOps.length > 0;
1437
+ useEffect3(() => {
1438
+ emitDirty(dirty);
1439
+ }, [dirty]);
1440
+ const applyPendingOps = useCallback(() => {
1441
+ const snapshot = store.getState();
1442
+ if (snapshot.document === null || snapshot.pendingOps.length === 0 || snapshot.status === "applying" || !editable) {
1443
+ return Promise.resolve();
1444
+ }
1445
+ store.dispatch({ type: "applyStarted" });
1446
+ return (async () => {
1447
+ try {
1448
+ const next = await engine.edit(snapshot.document, snapshot.pendingOps, {
1449
+ verify: true
1450
+ });
1451
+ const [nextEnvelope, pages, report] = await Promise.all([
1452
+ engine.read(next),
1453
+ engine.render(next, RENDER_SVG),
1454
+ engine.validate(next).catch(() => null)
1455
+ ]);
1456
+ setEnvelope(nextEnvelope);
1457
+ setValidation(report);
1458
+ store.dispatch({ type: "applySucceeded", document: next, pages });
1459
+ emitChange(next);
1460
+ } catch (e) {
1461
+ store.dispatch({ type: "applyFailed", error: toEditorError(e) });
1462
+ emitError(e);
1463
+ }
1464
+ })();
1465
+ }, [editable, engine, store]);
1466
+ const revert = useCallback(() => {
1467
+ const snapshot = store.getState();
1468
+ const previous = snapshot.snapshots[snapshot.snapshots.length - 1];
1469
+ if (previous === void 0) return Promise.resolve();
1470
+ return (async () => {
1471
+ try {
1472
+ const [nextEnvelope, pages, report] = await Promise.all([
1473
+ engine.read(previous),
1474
+ engine.render(previous, RENDER_SVG),
1475
+ engine.validate(previous).catch(() => null)
1476
+ ]);
1477
+ const now = store.getState();
1478
+ if (now.snapshots[now.snapshots.length - 1] !== previous) return;
1479
+ store.dispatch({ type: "undo" });
1480
+ setEnvelope(nextEnvelope);
1481
+ if (report !== null) setValidation(report);
1482
+ store.dispatch({ type: "setPages", pages });
1483
+ emitChange(previous);
1484
+ } catch (e) {
1485
+ setLoadError(toEditorError(e));
1486
+ emitError(e);
1487
+ }
1488
+ })();
1489
+ }, [engine, store]);
1490
+ const refresh = useCallback(async () => {
1491
+ const snapshot = store.getState();
1492
+ if (snapshot.document === null) return;
1493
+ try {
1494
+ const [nextEnvelope, pages, report] = await Promise.all([
1495
+ engine.read(snapshot.document),
1496
+ engine.render(snapshot.document, RENDER_SVG),
1497
+ engine.validate(snapshot.document).catch(() => null)
1498
+ ]);
1499
+ setEnvelope(nextEnvelope);
1500
+ setValidation(report);
1501
+ store.dispatch({ type: "setPages", pages });
1502
+ } catch (e) {
1503
+ setLoadError(toEditorError(e));
1504
+ emitError(e);
1505
+ throw e;
1506
+ }
1507
+ }, [engine, store]);
1508
+ const openCompose = useCallback(() => setComposing(true), []);
1509
+ useImperativeHandle(
1510
+ ref,
1511
+ () => ({ apply: applyPendingOps, revert, refresh, openCompose }),
1512
+ [applyPendingOps, revert, refresh, openCompose]
1513
+ );
1514
+ const contextValue = useMemo3(
1515
+ () => ({
1516
+ engine,
1517
+ store,
1518
+ state,
1519
+ envelope,
1520
+ capabilities,
1521
+ editable,
1522
+ validation,
1523
+ applyPendingOps,
1524
+ refresh,
1525
+ openCompose,
1526
+ t,
1527
+ // Always defined now, and stable: a widening, since every consumer
1528
+ // already calls it as `onError?.(e)` and it no-ops when the host
1529
+ // supplied no callback. The payload is unchanged.
1530
+ onError: emitError
1531
+ }),
1532
+ [
1533
+ engine,
1534
+ store,
1535
+ state,
1536
+ envelope,
1537
+ capabilities,
1538
+ editable,
1539
+ validation,
1540
+ applyPendingOps,
1541
+ refresh,
1542
+ openCompose,
1543
+ t
1544
+ ]
1545
+ );
1546
+ const onKeyDown = (e) => {
1547
+ if (e.key === "Escape") {
1548
+ store.dispatch({ type: "select", selection: null });
1549
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1550
+ e.preventDefault();
1551
+ void applyPendingOps();
1552
+ }
1553
+ };
1554
+ const rootClass = "hwped hwped-root" + (className !== void 0 ? ` ${className}` : "");
1555
+ return /* @__PURE__ */ jsx7(HwpEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs7(
1556
+ "div",
1557
+ {
1558
+ className: rootClass,
1559
+ style,
1560
+ onKeyDown,
1561
+ "data-status": state.status,
1562
+ lang: locale,
1563
+ children: [
1564
+ /* @__PURE__ */ jsxs7(
1565
+ "header",
1566
+ {
1567
+ className: "hwped-toolbar",
1568
+ role: "toolbar",
1569
+ "aria-label": t("toolbar.toolsAria"),
1570
+ children: [
1571
+ /* @__PURE__ */ jsx7("span", { className: "hwped-title", children: state.document?.name ?? "hwp-editor" }),
1572
+ !editable && /* @__PURE__ */ jsxs7(
1573
+ "span",
1574
+ {
1575
+ className: "hwped-notice",
1576
+ role: "note",
1577
+ ...capabilities?.reason !== void 0 ? { title: capabilities.reason } : {},
1578
+ children: [
1579
+ t("toolbar.readOnly"),
1580
+ capabilities?.editable === false && (capabilities.reason !== void 0 ? `: ${capabilities.reason}` : ` (${t("error.kind.protected")})`)
1581
+ ]
1582
+ }
1583
+ ),
1584
+ /* @__PURE__ */ jsx7("span", { className: "hwped-spacer" }),
1585
+ validation !== null && /* @__PURE__ */ jsx7(
1586
+ "span",
1587
+ {
1588
+ className: validation.valid ? "hwped-badge hwped-badge-ok" : "hwped-badge hwped-badge-err",
1589
+ role: "status",
1590
+ "aria-label": t("validation.aria"),
1591
+ children: validation.valid ? t("validation.valid") : t("validation.errors", { count: validation.errors.length })
1592
+ }
1593
+ ),
1594
+ /* @__PURE__ */ jsx7(
1595
+ "span",
1596
+ {
1597
+ className: "hwped-count",
1598
+ "aria-label": t("toolbar.pendingEdits", {
1599
+ count: state.pendingOps.length
1600
+ }),
1601
+ children: t("toolbar.pendingEdits", { count: state.pendingOps.length })
1602
+ }
1603
+ ),
1604
+ /* @__PURE__ */ jsx7(
1605
+ "button",
1606
+ {
1607
+ type: "button",
1608
+ className: "hwped-btn",
1609
+ onClick: () => void revert(),
1610
+ disabled: state.snapshots.length === 0 || state.status === "applying",
1611
+ children: t("toolbar.revert")
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ jsx7(
1615
+ "button",
1616
+ {
1617
+ type: "button",
1618
+ className: "hwped-btn hwped-btn-primary",
1619
+ onClick: () => void applyPendingOps(),
1620
+ disabled: !dirty || !editable || state.status === "applying",
1621
+ children: state.status === "applying" ? t("toolbar.applying") : t("toolbar.applyWithCount", {
1622
+ count: state.pendingOps.length
1623
+ })
1624
+ }
1625
+ ),
1626
+ /* @__PURE__ */ jsx7(
1627
+ "button",
1628
+ {
1629
+ type: "button",
1630
+ className: "hwped-btn",
1631
+ onClick: () => setComposing(true),
1632
+ children: t("toolbar.newDocument")
1633
+ }
1634
+ )
1635
+ ]
1636
+ }
1637
+ ),
1638
+ state.status === "error" && state.error !== null && /* @__PURE__ */ jsx7(ErrorLine, { prefix: t("error.prefix.apply"), ...state.error }),
1639
+ loadError !== null && /* @__PURE__ */ jsx7(ErrorLine, { prefix: t("error.prefix.load"), ...loadError }),
1640
+ /* @__PURE__ */ jsxs7("div", { className: "hwped-main", children: [
1641
+ state.document === null ? /* @__PURE__ */ jsx7(
1642
+ "div",
1643
+ {
1644
+ className: "hwped-canvas",
1645
+ role: "main",
1646
+ "aria-label": t("canvas.aria"),
1647
+ children: /* @__PURE__ */ jsxs7("div", { className: "hwped-empty", children: [
1648
+ /* @__PURE__ */ jsx7("p", { children: t("canvas.empty") }),
1649
+ /* @__PURE__ */ jsx7(
1650
+ "button",
1651
+ {
1652
+ type: "button",
1653
+ className: "hwped-btn hwped-btn-primary",
1654
+ onClick: () => setComposing(true),
1655
+ children: t("canvas.createCta")
1656
+ }
1657
+ )
1658
+ ] })
1659
+ }
1660
+ ) : /* @__PURE__ */ jsx7(PageCanvas, {}),
1661
+ /* @__PURE__ */ jsxs7("aside", { className: "hwped-side", "aria-label": t("side.panelAria"), children: [
1662
+ /* @__PURE__ */ jsx7("div", { className: "hwped-tabs", role: "tablist", children: [
1663
+ ["para", t("tabs.para")],
1664
+ ["table", t("tabs.table")],
1665
+ ["fields", t("tabs.fields")]
1666
+ ].map(([value, label]) => /* @__PURE__ */ jsx7(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ role: "tab",
1671
+ "aria-selected": tab === value,
1672
+ className: "hwped-tab" + (tab === value ? " hwped-tab-active" : ""),
1673
+ onClick: () => setTab(value),
1674
+ children: label
1675
+ },
1676
+ value
1677
+ )) }),
1678
+ tab === "para" && /* @__PURE__ */ jsx7(SegmentInspector, {}),
1679
+ tab === "table" && /* @__PURE__ */ jsx7(TableGrid, {}),
1680
+ tab === "fields" && /* @__PURE__ */ jsx7(FieldsPanel, {})
1681
+ ] })
1682
+ ] }),
1683
+ composing && /* @__PURE__ */ jsx7(
1684
+ ComposePanel,
1685
+ {
1686
+ onClose: () => setComposing(false),
1687
+ onComposed: (document) => {
1688
+ setComposing(false);
1689
+ emitChange(document);
1690
+ void (async () => {
1691
+ try {
1692
+ const [nextEnvelope, pages] = await Promise.all([
1693
+ engine.read(document),
1694
+ engine.render(document, RENDER_SVG)
1695
+ ]);
1696
+ setEnvelope(nextEnvelope);
1697
+ store.dispatch({ type: "load", document, pages });
1698
+ } catch (e) {
1699
+ setLoadError(toEditorError(e));
1700
+ emitError(e);
1701
+ }
1702
+ })();
1703
+ }
1704
+ }
1705
+ )
1706
+ ]
1707
+ }
1708
+ ) });
1709
+ }
1710
+ );
1711
+ export {
1712
+ COMPOSE_PRESETS,
1713
+ ComposePanel,
1714
+ FieldsPanel,
1715
+ HwpEditor,
1716
+ PageCanvas,
1717
+ SegmentInspector,
1718
+ TableGrid,
1719
+ bodyToBlocks,
1720
+ buildDocumentSpec,
1721
+ classifyEngineError,
1722
+ clickOffset,
1723
+ createT,
1724
+ en,
1725
+ extractFieldSlots,
1726
+ findTables,
1727
+ isTableSlice,
1728
+ ko,
1729
+ nearestSegment,
1730
+ parseMarkdownTable,
1731
+ plainSegmentText,
1732
+ sanitizeSvg,
1733
+ segmentBand,
1734
+ tableAtRef,
1735
+ useHwpEditorContext
1736
+ };
1737
+ //# sourceMappingURL=index.js.map